Public SDK packages, proxy-aware rate limits, service login recipe
- internal/{address,identity,protocol,tce,transport,verify} -> pkg/ so
external Go projects can import the verified core; invariant tests
updated for the new paths
- Config.TrustProxy: key rate limiting by X-Forwarded-For when the relay
sits behind a reverse proxy (off by default, header never trusted
otherwise)
- examples/service + examples/approve: complete passwordless login round
trip (mint request -> wallet approves -> local verify), run live in CI
- docs/SERVICE-GUIDE.md: the integration recipe
This commit is contained in:
parent
79df689f7a
commit
3bf13fa488
82 changed files with 797 additions and 110 deletions
|
|
@ -0,0 +1 @@
|
|||
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABQEg45x7MvYhHfhpEJeFV1ftnO/Oorhy/5W5sX5YYLSVStoAINBKsjJ0K7SrOhNovUYV5ObQIkq3GgFrr4UgozLJd4c3AfzsutQGECa+hvDDQGf4726neI5ispE=","signature":"zGL6tBvs5fO2Neur/nwV5AjhZGNquheAdtqTahBMvt44IekpPUuXdHqaiDNu/aPhClO7C4ijVlxxj98iY9MYAg==","object_id":"0e972deae436609ece1bf2aa80232fb6cf1b386bef5ed59cc0ee2abbf3de5cc7"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABAEAIOcA8DdWUiDbQo+nTu3ef0WKzCqMGhiXMYY2Sf5TT/koACCRvis7oAKfVr+F4lfOGDXOzB/0mI1pmhUeVjYrhlxaCARhdXRoAAx3cyBsaXZlIHRlc3SApKfaBrykp9oGEAcHBwcHBwcHBwcHBwcHBwc=","signature":"lnBnOW9Q5VuOtcCnxi1JXtRKV9z4Q86IouxwASclUzLRTdIsJ0evxq+/dBmIdt/6Hdxr5213UIGQVVn5HMkaCw==","object_id":"102ae6a0161833426efb7483cf7a2ff3c30f52b8c865520a136530566c33f823"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABAEAIILLyye2RTa7ii04SOM8iyDZw+veAHiwLX9LNytKirMKACDQSrIydCu0qzoTaL1GFeTm0CJKtxoBa6+FIKMyyXeHNwVsb2dpbgEHc2Vzc2lvbgMWS2pfWThqNVFCaUNFRmREUWd6QUtyQRdTaWduIGluIHRvIGRlbW8gc2VydmljZfbnutQGsui61AYQKB2UGcgF4ZNwWHdSmG0Etw==","signature":"3agPqlkbvKBpcIKf2fCz1QJxDPzLghlT583J/HTvcbpJesNkLtLWBmqoVopoqjW1qZcAyVkmShVSGYa1NTsKAA==","object_id":"1ab38c6aa921fb5eaaba099479341811e37afd7273d09d2e39c90192c532ee4c"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABAEAILOiOd/2wrH9Xd8KeVuTO6jprESVzIQdrry1RhALhETeACB/2ppLTnSq9hP6ZRY4K4olAOg94xHdULPB24QQh/PWvwRhdXRoABRhbmRyb2lkIHdzIGxpdmUgdGVzdICkp9oGvKSn2gYQBwcHBwcHBwcHBwcHBwcHBw==","signature":"0A62EzU0s8+MfWa+GPyd+J80xprG4orCajBD2rnupU5uet41luCCzFR3cB+TtzlE+Rrh+DrBq4tI3kpCLUUbAA==","object_id":"2745e5cba6f8c208f58e3eaac50c07ed871bd3977223767c1f8adf831467bbc7"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABAEAIILLyye2RTa7ii04SOM8iyDZw+veAHiwLX9LNytKirMKACDQSrIydCu0qzoTaL1GFeTm0CJKtxoBa6+FIKMyyXeHNwVsb2dpbgEHc2Vzc2lvbgMWbzY0TFpZemZROFJWUmNKUE83OUhtQRdTaWduIGluIHRvIGRlbW8gc2VydmljZe3nutQGqei61AYQY5R39BPmNKy6H7eeqdvN3w==","signature":"YOnrIHdzsIKyHrHZsNcQtSfiaKEvD6UchTOcvMOXZQm0GdJ4rEN2I71fhVpv/JP2Gt3sisuy/7MrrxFrd51AAw==","object_id":"618ff3cb6713ba3aecab174ca3037f714b90f5a2094323e30a94ee5abacb77bc"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABQEgyG2kH+/FGwQZKum7uHeiaHlM5CUUEiAu/crilN27sRcAINBKsjJ0K7SrOhNovUYV5ObQIkq3GgFrr4UgozLJd4c3Af/nutQGEKrodzQXiRYoNh/K6DU7x9o=","signature":"ba3ZEGaovxTLixjkP9KiDIrJ1zHGllW6mNgA3ORF/19m/M30XgaiJXbmCKtkQshi9E4xGD5+U/dmcAmPDJO7Aw==","object_id":"7d59b6aba3a2681f90c5c705f3fb0ab1c53323326839408d4dd351580a8a1972"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABQEgYY/zy2cTujrsqxdMowN/cUuQ9aIJQyPjCpTuWrrLd7wAINBKsjJ0K7SrOhNovUYV5ObQIkq3GgFrr4UgozLJd4c3Ae7nutQGEC3VyacYr+5LBo0xsHtvo1Y=","signature":"czfLYKfIMlzRXr3nDa+q934ciyYA00+nL+uA0pSRN+nFNHBUo/S5sFjPehP9oHYh0VUeklbObckrerdW5BtvAg==","object_id":"816d5d2e26f12f4f4ea339d53e8dcc36478e73bb5ecce03148c57890b44668ec"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEAAQEAIGgxl6UgyEP7hHyaXK+FCuY7aZMx2iIc0oJcWza8bVR2CHBvdy1saXZlgOLPqgY=","signature":"vu1W9QFDcH7R/KunZbzoJMwhm0on+UnqoqpyDd+W8AOV6gDg6L6dlTRdY6Ge/0OSl+lXufW+xgiRChrsh5y0CA==","object_id":"94764932c222671278b579b5cf8dbce706f71c023f9a11c32a61bf307bceab56"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEAAQEAIIJEncifJ40xeCa/nFA2Zy/eIBTNS8P7/s2R6pNesc6YCHBvdy1saXZlgOLPqgY=","signature":"WjY2lHununQjG0NOkNPzzyStsr3IxdiOToLNRlxP2YsuRUXpc0fvKho71H12hO+/3GYHZgKkP7IQvEWmD8NpAA==","object_id":"a902de693d1ea14571eb1786d3f7e545b7bb4e8e81226d00df2548972ebd4e85"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABAEAIILLyye2RTa7ii04SOM8iyDZw+veAHiwLX9LNytKirMKACCRvis7oAKfVr+F4lfOGDXOzB/0mI1pmhUeVjYrhlxaCAVsb2dpbgEHc2Vzc2lvbgMWTDlCTm9YaEVwcWd6bnhOUFl1UUhyQRdTaWduIGluIHRvIGRlbW8gc2VydmljZc3nutQGiei61AYQMbmTvcU0ct/YQH5qLSU48w==","signature":"mJigLGS2gykFOEQ/hvfo40Kca79ZNIgv8GtY2bWKU9gjJ+pjpjd3TgGd5R/ss++QNoROBcIqP/Ndpgiw5WX9Bw==","object_id":"c4a41dcadab39ba3b10d2f7749338e5c5f8c2ea4e86b589ff3067d28204ca153"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABAEAIILLyye2RTa7ii04SOM8iyDZw+veAHiwLX9LNytKirMKACDQSrIydCu0qzoTaL1GFeTm0CJKtxoBa6+FIKMyyXeHNwVsb2dpbgEHc2Vzc2lvbgMWeS04ak10enVvTnBUelluLVpmbmRPURdTaWduIGluIHRvIGRlbW8gc2VydmljZf/nutQGu+i61AYQZGJ4M7FvkiCnoTb/TFSwtw==","signature":"wVgK0dZ9qSBhimQzELmfdd52DYrK3nwP6Rv+ZNDHgrSKI4Kapww07O0E8BNRVhXagrZdMPtpjbSNJDEAhhDLDg==","object_id":"c86da41fefc51b04192ae9bbb877a268794ce4251412202efdcae294ddbbb117"}
|
||||
1
data/checkpoint-1.json
Normal file
1
data/checkpoint-1.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"bytes":"dHJ1c3QubjFrby5kZXYvY2twdC8xAAEBASCa4Wwr3EzcvPeg2HpkwiKrsJdV9xapyWTyt1Iuis+NpSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAODst9QG","signature":"cANWA+qCI4aANYhUpsN0cwgtnOb8sgGBcphmydAG/XJYQnWCbOtRLeInsf6cHxt64Oms7962/Zvdgs4mjxn/Ag==","id":"29080de9e64a75de4453a96f5c4294bcc3b3d81c4402a664ebb3a2f94e657c33"}
|
||||
1
data/checkpoint-2.json
Normal file
1
data/checkpoint-2.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"bytes":"dHJ1c3QubjFrby5kZXYvY2twdC8xAAECAiAO+gsIekCmIn1LaLOZVZdaK2JWBBKupWpvU4CIAkLEayApCA3p5kp13kRTqW9cQpS8w7PYHEQCpmTrs6L5TmV8M7f0t9QG","signature":"Rx9wLAo5NFvv3NNuOzXyB3MwUWU3u3/uLHKlHlraz+bxCVVI+So6Cvh5MVNvjBDf92Sg6Np+5Lb2NMZaNp7JDQ==","id":"6ae384a485308701adf6f0da2767266ff90f0c25b024ba706fdd1c60ca010f6c"}
|
||||
1
data/checkpoint_meta.json
Normal file
1
data/checkpoint_meta.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"epoch":2,"last_hash":"6ae384a485308701adf6f0da2767266ff90f0c25b024ba706fdd1c60ca010f6c"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABAEAIPMtJyRnRoKrwG4/7YYYNg+Q/X358s7n7K4cPkcz3PtjACDVXkf9iQ8Zx+mWZW8YMFy/f/Xk6TpYsr5C0v8ldWigUwRhdXRoAAx3cyBsaXZlIHRlc3SApKfaBrykp9oGEAcHBwcHBwcHBwcHBwcHBwc=","signature":"EF6/T3cGoF4HJ47yGM9QfMB1eilnvbrSg8vtqupLF2f+zOO1fflYKmQMXBi8RJyrX/pJTMBdZspH3sJkQcFiAw==","object_id":"d8a462d2755ffb16d8bc615e78db232289f571988b158febf190f71a969153d8"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABAEAILZvMfVh6H5uBBpoA570y5ZKs5TXNc6U5U0ff9wVJfltACDQSrIydCu0qzoTaL1GFeTm0CJKtxoBa6+FIKMyyXeHNwVsb2dpbgEHc2Vzc2lvbgMWVHBqTDNFZkdhS20wczZ0TXpQQTU4ZxdTaWduIGluIHRvIGRlbW8gc2VydmljZfzsutQGuO261AYQA8GUwWqDoGyqy9Y0E83RhQ==","signature":"wG7vho7wxIMqO7205aFzbhAtpkHvRSHhrLvs+iKYyk51MNZZZgCOF4LFk2hFv3SiOkxRnm5rcWwgjyqGK5AcDA==","object_id":"e39c7b32f6211df8691097855757ed9cefcea2b872ff95b9b17e5860b4954ada"}
|
||||
2
data/relay_key.seed
Normal file
2
data/relay_key.seed
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<EFBFBD>ɬ3<EFBFBD><EFBFBD><EFBFBD>
|
||||
<EFBFBD>6;k<><6B>j9h<39>L<EFBFBD><4C>R\<5C>}m<><1C>7
|
||||
121
docs/SERVICE-GUIDE.md
Normal file
121
docs/SERVICE-GUIDE.md
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# SERVICE-GUIDE.md — вход по Niko Trust в вашем сервисе
|
||||
|
||||
Рецепт для владельца сервиса: как принимать Niko Trust как основной способ
|
||||
аутентификации. Полный рабочий код — [`examples/service`](../examples/service)
|
||||
(сервис) и [`examples/approve`](../examples/approve) (роль кошелька).
|
||||
|
||||
## Модель в двух абзацах
|
||||
|
||||
Идентичность — это пара ключей Ed25519; адрес `trust1…` выведен из публичного
|
||||
ключа (bech32). Сервис не видит паролей: он публикует **ApprovalRequest** —
|
||||
подписанный вопрос «войти?» адресованный конкретному адресу, действительный
|
||||
≤60 секунд. Владелец кошелька (приложение niko_trust_gui / Android) отвечает
|
||||
**ApprovalResponse**, который криптографически привязан к байтам именно этого
|
||||
запроса (INV-4). Релей — тупое хранилище конвертов: он не проверяет подписи и
|
||||
не знает, кто прав; всю верификацию сервис делает сам локально.
|
||||
|
||||
Что это даёт по безопасности: фишинг невозможен (запрос подписан ключом
|
||||
сервиса и показывается в кошельке с его адресом), повтор невозможен (ответ
|
||||
умирает вместе с окном запроса), подмена решения невозможна (ответ подписан
|
||||
ключом пользователя и привязан к хешу запроса), а компрометация релея не даёт
|
||||
атакующему ничего, кроме отказа в обслуживании.
|
||||
|
||||
## Поток логина
|
||||
|
||||
```
|
||||
браузер ваш сервис релей кошелёк
|
||||
| GET /login?user=X | | |
|
||||
|----------------------->| ApprovalRequest(action= | |
|
||||
| | "login", recipient=X) | |
|
||||
| |-- POST /v1/objects ------->| |
|
||||
|<- { id } --------------| |--- push ----->|
|
||||
| | GET /v1/responses?request=id (или WS) |
|
||||
| ...пользователь жмёт «Разрешить» в кошельке... |<-- store -----|
|
||||
| GET /login/status |<-- envelope {tce,sig} -----| |
|
||||
|----------------------->| VerifyApprovalResponse() | |
|
||||
|<- {approved, user:X} --| → своя сессия | |
|
||||
```
|
||||
|
||||
## Шаги
|
||||
|
||||
### 1. Идентичность сервиса
|
||||
|
||||
Сгенерируйте один раз ключ и храните сид как секрет (это «сертификат»
|
||||
сервиса):
|
||||
|
||||
```go
|
||||
sv, _ := signer.Generate()
|
||||
fmt.Println(sv.Address(), hex.EncodeToString(sv.Seed()))
|
||||
// при старте: signer.FromSeed(seedBytes)
|
||||
```
|
||||
|
||||
Адрес сервиса увидит пользователь в кошельке рядом с текстом запроса.
|
||||
|
||||
### 2. Создание запроса на вход
|
||||
|
||||
```go
|
||||
req := &protocol.ApprovalRequest{
|
||||
Sender: sv.Public(),
|
||||
Recipient: []byte(userAddr.PubKey()), // чей это вход
|
||||
Action: "login",
|
||||
Payload: map[string]tce.Value{"session": tce.String(sessionID)},
|
||||
Message: "Sign in to demo service", // человек это прочитает
|
||||
CreatedAt: uint64(time.Now().Unix()),
|
||||
ExpiresAt: uint64(time.Now().Unix() + 60),
|
||||
Nonce: nonce16(),
|
||||
}
|
||||
tceBytes, _ := protocol.EncodeApprovalRequest(req)
|
||||
sig := sv.Sign(tceBytes)
|
||||
id := tce.ComputeID(tceBytes) // = object id, им же ссылается ответ
|
||||
POST {base}/v1/objects {"tce": b64(tceBytes), "signature": b64(sig)}
|
||||
```
|
||||
|
||||
`Recipient` обязан быть валидным адресом: запрос всегда адресный.
|
||||
|
||||
### 3. Ожидание ответа
|
||||
|
||||
Два способа:
|
||||
|
||||
- **Опрос** (просто): `GET /v1/responses?request=<hex id>` каждые ~0.7 c.
|
||||
Требует сессию чтения — см. шаг 5.
|
||||
- **WebSocket** (правильно): подключиться к `GET /v1/ws` со своим токеном и
|
||||
отправить `{"op":"subscribe","channel":"responses","key":"<hex id>"}` —
|
||||
события приходят мгновенно (docs/API.md §WebSocket).
|
||||
|
||||
### 4. Верификация — единственная критичная строка
|
||||
|
||||
```go
|
||||
resp, err := protocol.VerifyApprovalResponse(reqTCE, reqSig, respTCE, respSig)
|
||||
if err == nil && resp.Decision == protocol.Allow &&
|
||||
bytes.Equal(resp.Responder, []byte(userAddr.PubKey())) {
|
||||
// адрес верифицирован → выпустить свою сессию
|
||||
}
|
||||
```
|
||||
|
||||
Проверка уже включает: строгий декод обоих объектов, подпись под ключом
|
||||
ответившего, привязку к точным байтам вашего запроса и окно времени.
|
||||
Сверка `Responder` с ожидаемым адресом отсекает ответы чужих кошельков.
|
||||
|
||||
### 5. Сессия чтения релея
|
||||
|
||||
Чтение лент требует токена: `POST /v1/auth/challenge` → подписать
|
||||
`AuthAssertion` (audience взять из `GET /v1/config`) → `POST /v1/auth/assert`
|
||||
→ `session_token`, живёт 30 минут (docs/API.md). Готовые реализации:
|
||||
`pkg/protocol.VerifyAuthAssertion`-клиенты в примерах выше.
|
||||
|
||||
## Где брать готовый клиентский код
|
||||
|
||||
| Язык | Пакет |
|
||||
|-----------|-----------------------------------------------------------------------|
|
||||
| Go | этот модуль: `pkg/tce`, `pkg/protocol`, `pkg/verify`, `pkg/address`, `pkg/identity/signer` |
|
||||
| Rust | крейт `niko_trust_gui` (git-зависимость), модули `tce`, `protocol`, `relay`; пример `examples/service_login.rs` |
|
||||
| Kotlin/JVM| модуль `:sdk` репозитория niko_trust_android (`RelayClient`, `LiveFeed`, протокол) |
|
||||
|
||||
## Эксплуатация
|
||||
|
||||
- Релей за обратным прокси? Включите `trust_proxy: true` в config.yaml, иначе
|
||||
рейт-лимиты считают всех одним IP.
|
||||
- Рестарт релея сбрасывает сессии — клиенты перекладываются сами; ваш сервис
|
||||
должен переживать 502/401 во время окна рестарта.
|
||||
- Храните сид сервиса как секрет: он подписывает все запросы, и пользователь
|
||||
видит его адрес в каждом подтверждении.
|
||||
131
examples/approve/main.go
Normal file
131
examples/approve/main.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
// Command approve acts as the user's wallet for the demo: it fetches an
|
||||
// ApprovalRequest from the relay by id, checks it is addressed to us, signs
|
||||
// an ApprovalResponse and stores it. Combined with examples/service this
|
||||
// exercises a full login round trip on one machine.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
var (
|
||||
relayURL = flag.String("relay", "http://127.0.0.1:8080", "relay base URL")
|
||||
idHex = flag.String("id", "", "request id (hex) to answer")
|
||||
deny = flag.Bool("deny", false, "deny instead of allow")
|
||||
seedHex = flag.String("seed", "", "user identity seed (hex); generated when empty")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *idHex == "" {
|
||||
log.Fatal("-id is required")
|
||||
}
|
||||
|
||||
var user *signer.Signer
|
||||
var err error
|
||||
if *seedHex == "" {
|
||||
user, err = signer.Generate()
|
||||
} else {
|
||||
b, e := hex.DecodeString(*seedHex)
|
||||
if e != nil || len(b) != ed25519SeedSize {
|
||||
log.Fatal("-seed must be 64 hex characters")
|
||||
}
|
||||
user, err = signer.FromSeed(b)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("wallet identity:", user.Address())
|
||||
|
||||
// Fetch the exact request bytes by their content address. A hostile
|
||||
// relay can substitute anything here — that is why we verify instead of
|
||||
// just decoding.
|
||||
reqTCE, reqSig := getObject(*relayURL, *idHex)
|
||||
req, err := protocol.VerifyApprovalRequest(reqTCE, reqSig)
|
||||
if err != nil {
|
||||
log.Fatalf("request does not verify: %v", err)
|
||||
}
|
||||
if string(req.Recipient) != string(user.Public()) {
|
||||
log.Fatal("this request is not addressed to our identity")
|
||||
}
|
||||
fmt.Printf("action=%q message=%q\n", req.Action, req.Message)
|
||||
|
||||
decision := protocol.Allow
|
||||
if *deny {
|
||||
decision = protocol.Deny
|
||||
}
|
||||
resp := &protocol.ApprovalResponse{
|
||||
RequestHash: tce.ComputeID(reqTCE), // INV-4: commits to the exact bytes
|
||||
Responder: user.Public(),
|
||||
Decision: decision,
|
||||
CreatedAt: uint64(time.Now().Unix()),
|
||||
Nonce: nonce(),
|
||||
}
|
||||
respTCE, err := protocol.EncodeApprovalResponse(resp)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
out := post(*relayURL+"/v1/objects",
|
||||
fmt.Sprintf(`{"tce":%q,"signature":%q}`,
|
||||
base64.StdEncoding.EncodeToString(respTCE),
|
||||
base64.StdEncoding.EncodeToString(user.Sign(respTCE))))
|
||||
fmt.Printf("response stored: %s\n", strings.TrimSpace(out))
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- tiny http io
|
||||
|
||||
func getObject(base, id string) ([]byte, []byte) {
|
||||
httpResp, err := http.Get(base + "/v1/objects/" + id)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
raw, _ := io.ReadAll(httpResp.Body)
|
||||
var env struct {
|
||||
Tce string `json:"tce"`
|
||||
Signature string `json:"signature"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
json.Unmarshal(raw, &env)
|
||||
if env.Tce == "" {
|
||||
log.Fatalf("fetch object %s: %s", id, env.Error)
|
||||
}
|
||||
tceB, e1 := base64.StdEncoding.DecodeString(env.Tce)
|
||||
sigB, e2 := base64.StdEncoding.DecodeString(env.Signature)
|
||||
if e1 != nil || e2 != nil {
|
||||
log.Fatal("bad envelope encoding")
|
||||
}
|
||||
return tceB, sigB
|
||||
}
|
||||
|
||||
func post(url, body string) string {
|
||||
httpResp, err := http.Post(url, "application/json", strings.NewReader(body))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
b, _ := io.ReadAll(httpResp.Body)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func nonce() []byte {
|
||||
n := make([]byte, tce.NonceSize)
|
||||
rand.Read(n)
|
||||
return n
|
||||
}
|
||||
|
||||
const ed25519SeedSize = 32
|
||||
362
examples/service/main.go
Normal file
362
examples/service/main.go
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
// Command service is a complete minimal "log in with Niko Trust" service.
|
||||
//
|
||||
// Flow (docs/SERVICE-GUIDE.md):
|
||||
//
|
||||
// GET /login -> mint an ApprovalRequest, store it on the relay,
|
||||
// return { id } and start watching for a response
|
||||
// GET /login/status -> pending | approved <address> | denied | expired
|
||||
//
|
||||
// The user sees the request in their wallet app and taps approve; the relay
|
||||
// stores the signed ApprovalResponse; this service fetches it back and
|
||||
// verifies it locally against its own request bytes — nothing about the
|
||||
// decision is taken on trust.
|
||||
//
|
||||
// Try it end-to-end:
|
||||
//
|
||||
// go run ./examples/service -relay http://127.0.0.1:8080 -http 127.0.0.1:9090 &
|
||||
// curl -s localhost:9090/login # -> {"id": "..."}
|
||||
// go run ./examples/approve -relay http://127.0.0.1:8080 \
|
||||
// -id <id from login> # act as the user
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
var (
|
||||
relayURL = flag.String("relay", "http://127.0.0.1:8080", "relay base URL")
|
||||
httpAddr = flag.String("http", "127.0.0.1:9090", "listen address of this demo service")
|
||||
seedHex = flag.String("seed", "", "service identity seed (hex, 64 chars); generated when empty")
|
||||
)
|
||||
|
||||
type pending struct {
|
||||
req *protocol.ApprovalRequest
|
||||
reqTCE []byte
|
||||
reqSig []byte
|
||||
result chan string // user address once approved; "" on deny/expire
|
||||
deadline time.Time
|
||||
}
|
||||
|
||||
type service struct {
|
||||
signer *signer.Signer
|
||||
token string // relay session for read endpoints
|
||||
pending sync.Map // request id hex -> *pending
|
||||
}
|
||||
|
||||
// authenticate performs the challenge/assert handshake (docs/API.md) and
|
||||
// returns a session token with read access.
|
||||
func authenticate(base string, sv *signer.Signer) string {
|
||||
cfg := getJSON(base + "/v1/config")
|
||||
ch := postJSON(base+"/v1/auth/challenge", "{}")
|
||||
chal, err := hex.DecodeString(ch["challenge"].(string))
|
||||
if err != nil || len(chal) != tce.ChallengeSize {
|
||||
log.Fatal("bad challenge from relay")
|
||||
}
|
||||
a := &protocol.AuthAssertion{
|
||||
PubKey: sv.Public(),
|
||||
Challenge: chal,
|
||||
Scope: "read",
|
||||
Audience: cfg["audience"].(string),
|
||||
CreatedAt: uint64(time.Now().Unix()),
|
||||
}
|
||||
aTCE, err := protocol.EncodeAuthAssertion(a)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
body := fmt.Sprintf(`{"tce":%q,"signature":%q}`,
|
||||
base64.StdEncoding.EncodeToString(aTCE),
|
||||
base64.StdEncoding.EncodeToString(sv.Sign(aTCE)))
|
||||
out := post(base+"/v1/auth/assert", body)
|
||||
var doc struct {
|
||||
SessionToken string `json:"session_token"`
|
||||
}
|
||||
json.Unmarshal([]byte(out), &doc)
|
||||
if doc.SessionToken == "" {
|
||||
log.Fatalf("auth assert failed: %s", out)
|
||||
}
|
||||
return doc.SessionToken
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var sv *signer.Signer
|
||||
var err error
|
||||
if *seedHex == "" {
|
||||
sv, err = signer.Generate()
|
||||
} else {
|
||||
b, e := hex.DecodeString(*seedHex)
|
||||
if e != nil || len(b) != ed25519.SeedSize {
|
||||
log.Fatal("-seed must be exactly 64 hex characters")
|
||||
}
|
||||
sv, err = signer.FromSeed(b)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("service identity: %s", sv.Address())
|
||||
|
||||
token := authenticate(*relayURL, sv)
|
||||
log.Printf("relay session established")
|
||||
|
||||
svc := &service{signer: sv, token: token}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /login", svc.startLogin)
|
||||
mux.HandleFunc("GET /login/status", svc.loginStatus)
|
||||
log.Printf("listening on http://%s", *httpAddr)
|
||||
log.Fatal(http.ListenAndServe(*httpAddr, mux))
|
||||
}
|
||||
|
||||
// startLogin mints a fresh login challenge addressed to one user:
|
||||
// GET /login?user=trust1...
|
||||
//
|
||||
// Only the holder of that identity can produce a valid ApprovalResponse, so
|
||||
// a verified "approved" reply is proof the account owner consents — exactly
|
||||
// the property a passwordless login needs.
|
||||
func (s *service) startLogin(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := address.Parse(r.URL.Query().Get("user"))
|
||||
if err != nil {
|
||||
httpError(w, 400, fmt.Errorf("missing or bad ?user=<trust address>"))
|
||||
return
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
session := make([]byte, 16)
|
||||
if _, err := rand.Read(session); err != nil {
|
||||
httpError(w, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := &protocol.ApprovalRequest{
|
||||
Sender: s.signer.Public(),
|
||||
Recipient: []byte(user.PubKey()),
|
||||
Action: "login",
|
||||
Payload: map[string]tce.Value{
|
||||
"session": tce.String(base64.RawURLEncoding.EncodeToString(session)),
|
||||
},
|
||||
Message: "Sign in to demo service",
|
||||
CreatedAt: uint64(now),
|
||||
ExpiresAt: uint64(now + 60), // protocol caps approval windows at 60 s
|
||||
Nonce: nonce(),
|
||||
}
|
||||
reqTCE, err := protocol.EncodeApprovalRequest(req)
|
||||
if err != nil {
|
||||
httpError(w, 500, err)
|
||||
return
|
||||
}
|
||||
reqSig := s.signer.Sign(reqTCE)
|
||||
|
||||
_, code, body := postEnvelope(*relayURL, reqTCE, reqSig)
|
||||
if code != 200 {
|
||||
httpError(w, 502, fmt.Errorf("relay store: %d %s", code, body))
|
||||
return
|
||||
}
|
||||
|
||||
reqID := tce.ComputeID(reqTCE)
|
||||
id := hex.EncodeToString(reqID[:])
|
||||
s.pending.Store(id, &pending{
|
||||
req: req,
|
||||
reqTCE: reqTCE,
|
||||
reqSig: reqSig,
|
||||
result: make(chan string, 1),
|
||||
deadline: time.Now().Add(70 * time.Second),
|
||||
})
|
||||
go s.watch(id)
|
||||
|
||||
writeJSON(w, map[string]string{"id": id})
|
||||
}
|
||||
|
||||
// watch polls the relay until the request is answered or expires. A
|
||||
// production service would subscribe to the `responses` WebSocket channel of
|
||||
// the relay instead of polling (docs/API.md §WebSocket).
|
||||
func (s *service) watch(id string) {
|
||||
v, _ := s.pending.Load(id)
|
||||
p := v.(*pending)
|
||||
for time.Now().Before(p.deadline) {
|
||||
time.Sleep(700 * time.Millisecond)
|
||||
tceB64, sigB64, ok := fetchResponse(*relayURL, s.token, id)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
respTCE, e1 := base64.StdEncoding.DecodeString(tceB64)
|
||||
respSig, e2 := base64.StdEncoding.DecodeString(sigB64)
|
||||
if e1 != nil || e2 != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// The whole security model lives in this one call: strict decode of
|
||||
// both objects, signature under the responder key, binding to our
|
||||
// exact request bytes.
|
||||
resp, err := protocol.VerifyApprovalResponse(p.reqTCE, p.reqSig, respTCE, respSig)
|
||||
if err != nil {
|
||||
continue // hostile or mismatched envelope: keep waiting
|
||||
}
|
||||
if !bytes.Equal(resp.Responder, p.req.Recipient) {
|
||||
continue // someone else answered; not our user
|
||||
}
|
||||
user, err := address.FromPubKey(resp.Responder)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
answer := ""
|
||||
if resp.Decision == protocol.Allow {
|
||||
answer = user.String()
|
||||
}
|
||||
select {
|
||||
case p.result <- answer:
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
select {
|
||||
case p.result <- "":
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// loginStatus reports the outcome for one login attempt.
|
||||
func (s *service) loginStatus(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
v, ok := s.pending.Load(id)
|
||||
if !ok {
|
||||
httpError(w, 404, fmt.Errorf("unknown id"))
|
||||
return
|
||||
}
|
||||
p := v.(*pending)
|
||||
select {
|
||||
case user := <-p.result:
|
||||
if user == "" {
|
||||
writeJSON(w, map[string]string{"status": "denied"})
|
||||
return
|
||||
}
|
||||
// A real service would issue its own session cookie bound to `user`
|
||||
// here. The verified address IS the identity; nothing else is needed.
|
||||
writeJSON(w, map[string]string{"status": "approved", "user": user})
|
||||
default:
|
||||
if time.Now().After(p.deadline) {
|
||||
writeJSON(w, map[string]string{"status": "expired"})
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]string{"status": "pending"})
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- http helpers
|
||||
|
||||
func writeJSON(w http.ResponseWriter, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func httpError(w http.ResponseWriter, code int, err error) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- transport
|
||||
|
||||
// postObject stores an envelope; returns object id, status, body.
|
||||
func postEnvelope(base string, tceBytes, sig []byte) (string, int, string) {
|
||||
body := fmt.Sprintf(`{"tce":%q,"signature":%q}`,
|
||||
base64.StdEncoding.EncodeToString(tceBytes),
|
||||
base64.StdEncoding.EncodeToString(sig))
|
||||
resp, err := http.Post(base+"/v1/objects", "application/json", strings.NewReader(body))
|
||||
if err != nil {
|
||||
return "", 0, err.Error()
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
var out struct {
|
||||
ObjectID string `json:"object_id"`
|
||||
Raw string `json:"raw"`
|
||||
}
|
||||
json.Unmarshal(raw, &out) // best effort; raw keeps the error text
|
||||
if out.ObjectID != "" {
|
||||
return out.ObjectID, resp.StatusCode, ""
|
||||
}
|
||||
return "", resp.StatusCode, string(raw)
|
||||
}
|
||||
|
||||
// post sends a raw JSON body and returns the response text.
|
||||
func post(url, body string) string {
|
||||
httpResp, err := http.Post(url, "application/json", strings.NewReader(body))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
raw, _ := io.ReadAll(httpResp.Body)
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
// postJSON posts an empty/raw JSON body and parses the reply.
|
||||
func postJSON(url, body string) map[string]any {
|
||||
raw := post(url, body)
|
||||
var m map[string]any
|
||||
json.Unmarshal([]byte(raw), &m)
|
||||
return m
|
||||
}
|
||||
|
||||
// getJSON fetches a small JSON object endpoint.
|
||||
func getJSON(url string) map[string]any {
|
||||
httpResp, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
raw, _ := io.ReadAll(httpResp.Body)
|
||||
var m map[string]any
|
||||
json.Unmarshal(raw, &m)
|
||||
return m
|
||||
}
|
||||
|
||||
// fetchResponse looks up the first response envelope for a request id.
|
||||
func fetchResponse(base, token, idHex string) (tceB64, sigB64 string, ok bool) {
|
||||
req, err := http.NewRequest("GET", base+"/v1/responses?request="+idHex, nil)
|
||||
if err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return "", "", false
|
||||
}
|
||||
var doc struct {
|
||||
Responses []struct {
|
||||
Tce string `json:"tce"`
|
||||
Signature string `json:"signature"`
|
||||
} `json:"responses"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&doc)
|
||||
if len(doc.Responses) == 0 {
|
||||
return "", "", false
|
||||
}
|
||||
return doc.Responses[0].Tce, doc.Responses[0].Signature, true
|
||||
}
|
||||
|
||||
func nonce() []byte {
|
||||
n := make([]byte, tce.NonceSize)
|
||||
rand.Read(n)
|
||||
return n
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"os"
|
||||
"time"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
|
|
@ -28,6 +28,13 @@ type Config struct {
|
|||
SessionTTL time.Duration // validity of a verified auth session
|
||||
|
||||
MaxBodyBytes int64 // hard cap on any request body
|
||||
|
||||
// TrustProxy: when true, rate limiting keys clients by the
|
||||
// X-Forwarded-For header sent by the reverse proxy in front of the
|
||||
// relay instead of the socket address (which would collapse every
|
||||
// visitor into one bucket). Only enable it when the relay is actually
|
||||
// reachable exclusively through a proxy you control.
|
||||
TrustProxy bool
|
||||
}
|
||||
|
||||
// configFile mirrors Config but keeps durations as strings so they can be
|
||||
|
|
@ -45,6 +52,7 @@ type configFile struct {
|
|||
ChallengeTTL string `yaml:"challenge_ttl"`
|
||||
SessionTTL string `yaml:"session_ttl"`
|
||||
MaxBodyBytes int64 `yaml:"max_body_bytes"`
|
||||
TrustProxy bool `yaml:"trust_proxy"`
|
||||
}
|
||||
|
||||
// DefaultConfig returns the built-in defaults.
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ func TestServerDoesNotImportSigner(t *testing.T) {
|
|||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if p == "git.n1ko.dev/Niko/niko_trust/internal/identity/signer" {
|
||||
if p == "git.n1ko.dev/Niko/niko_trust/pkg/identity/signer" {
|
||||
t.Errorf("%s imports the signer package, violating INV-1", e.Name())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,10 +51,41 @@ func (l *ipLimiter) allow(ip string) bool {
|
|||
}
|
||||
|
||||
// clientIP returns the request's remote IP, stripping a port if present.
|
||||
func clientIP(r *http.Request) string {
|
||||
// With trustProxy set (Config.TrustProxy) the left-most X-Forwarded-For
|
||||
// entry wins: the relay sits behind a reverse proxy and every socket would
|
||||
// otherwise share the proxy's address. The header is attacker-controlled,
|
||||
// which is exactly why trusting it is an explicit operator choice.
|
||||
func clientIP(r *http.Request, trustProxy bool) string {
|
||||
if trustProxy {
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
if i := indexByte(xff, ','); i >= 0 {
|
||||
xff = xff[:i]
|
||||
}
|
||||
return trimSpace(xff)
|
||||
}
|
||||
}
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
func indexByte(s string, b byte) int {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == b {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func trimSpace(s string) string {
|
||||
for len(s) > 0 && (s[0] == ' ' || s[0] == '\t') {
|
||||
s = s[1:]
|
||||
}
|
||||
for len(s) > 0 && (s[len(s)-1] == ' ' || s[len(s)-1] == '\t') {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package server
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
|
@ -48,13 +49,26 @@ func TestWindowReset(t *testing.T) {
|
|||
func TestClientIP(t *testing.T) {
|
||||
withPort, _ := http.NewRequest(http.MethodGet, "/", nil)
|
||||
withPort.RemoteAddr = "192.168.1.5:54321"
|
||||
if got := clientIP(withPort); got != "192.168.1.5" {
|
||||
if got := clientIP(withPort, false); got != "192.168.1.5" {
|
||||
t.Fatalf("expected 192.168.1.5, got %q", got)
|
||||
}
|
||||
|
||||
noPort, _ := http.NewRequest(http.MethodGet, "/", nil)
|
||||
noPort.RemoteAddr = "10.0.0.1"
|
||||
if got := clientIP(noPort); got != "10.0.0.1" {
|
||||
if got := clientIP(noPort, false); got != "10.0.0.1" {
|
||||
t.Fatalf("expected 10.0.0.1, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientIPTrustsForwardedFor(t *testing.T) {
|
||||
r := httptest.NewRequest("POST", "/v1/objects", nil)
|
||||
r.RemoteAddr = "10.0.0.9:55555"
|
||||
r.Header.Set("X-Forwarded-For", "203.0.113.7, 10.0.0.1")
|
||||
if got := clientIP(r, true); got != "203.0.113.7" {
|
||||
t.Fatalf("trusted proxy: got %q, want 203.0.113.7", got)
|
||||
}
|
||||
// Without the knob the header must be ignored (spoofable).
|
||||
if got := clientIP(r, false); got != "10.0.0.9" {
|
||||
t.Fatalf("untrusted: got %q, want 10.0.0.9", got)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/transport"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/transport"
|
||||
)
|
||||
|
||||
// Server is the trust relay: it stores signed objects and brokers
|
||||
|
|
@ -30,6 +30,7 @@ type Server struct {
|
|||
maxBodyBytes int64
|
||||
challengeTTL time.Duration
|
||||
sessionTTL time.Duration
|
||||
trustProxy bool
|
||||
|
||||
putLimiter *ipLimiter
|
||||
challengeLimiter *ipLimiter
|
||||
|
|
@ -91,6 +92,7 @@ func New(cfg Config) *Server {
|
|||
maxBodyBytes: cfg.MaxBodyBytes,
|
||||
challengeTTL: cfg.ChallengeTTL,
|
||||
sessionTTL: cfg.SessionTTL,
|
||||
trustProxy: cfg.TrustProxy,
|
||||
challenges: make(map[string]time.Time),
|
||||
sessions: make(map[string]session),
|
||||
ws: newWSHub(),
|
||||
|
|
@ -125,7 +127,7 @@ func (s *Server) Handler() http.Handler {
|
|||
}
|
||||
|
||||
func (s *Server) rateLimitPut(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.putLimiter.allow(clientIP(r)) {
|
||||
if !s.putLimiter.allow(clientIP(r, s.trustProxy)) {
|
||||
writeErr(w, http.StatusTooManyRequests, "rate limited")
|
||||
return
|
||||
}
|
||||
|
|
@ -133,7 +135,7 @@ func (s *Server) rateLimitPut(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func (s *Server) rateLimitChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.challengeLimiter.allow(clientIP(r)) {
|
||||
if !s.challengeLimiter.allow(clientIP(r, s.trustProxy)) {
|
||||
writeErr(w, http.StatusTooManyRequests, "rate limited")
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/server"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/transport"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/verify"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/transport"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/verify"
|
||||
)
|
||||
|
||||
func newTestServer(t *testing.T) *httptest.Server {
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/transport"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/transport"
|
||||
)
|
||||
|
||||
// Store is a content-addressed, in-memory store of signed objects.
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ import (
|
|||
|
||||
"github.com/coder/websocket"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/transport"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/transport"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ import (
|
|||
|
||||
"github.com/coder/websocket"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/transport"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/transport"
|
||||
)
|
||||
|
||||
func wsURL(httpURL string) string {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
|
||||
"github.com/btcsuite/btcd/btcutil/bech32"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/address"
|
||||
)
|
||||
|
||||
// deterministicKey returns a reproducible valid Ed25519 public key.
|
||||
|
|
@ -5,7 +5,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/address"
|
||||
)
|
||||
|
||||
func seedCorpus(f *testing.F) {
|
||||
|
|
@ -5,7 +5,7 @@ import (
|
|||
|
||||
"github.com/btcsuite/btcd/btcutil/bech32"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/address"
|
||||
)
|
||||
|
||||
// encodeRawForTest builds a syntactically valid bech32m trust address around
|
||||
|
|
@ -5,7 +5,7 @@ import (
|
|||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/address"
|
||||
)
|
||||
|
||||
// smallOrderKeys is the standard list of Edwards25519 points of order 1, 2, 4
|
||||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity"
|
||||
)
|
||||
|
||||
func TestAliasAccepts(t *testing.T) {
|
||||
|
|
@ -4,8 +4,8 @@ import (
|
|||
"crypto/ed25519"
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity/signer"
|
||||
)
|
||||
|
||||
// FuzzVerifyNeverPanics asserts that verification is total over arbitrary
|
||||
|
|
@ -32,7 +32,7 @@ import (
|
|||
"crypto/ed25519"
|
||||
"errors"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/address"
|
||||
)
|
||||
|
||||
// ErrInvalidSignature is returned when a signature does not verify.
|
||||
|
|
@ -5,9 +5,9 @@ import (
|
|||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity/signer"
|
||||
)
|
||||
|
||||
func mustSigner(t *testing.T) *signer.Signer {
|
||||
|
|
@ -84,8 +84,8 @@ func TestProtocolLayerImports(t *testing.T) {
|
|||
}
|
||||
|
||||
protocolPackages := []string{
|
||||
modulePath + "/internal/address",
|
||||
modulePath + "/internal/identity",
|
||||
modulePath + "/pkg/address",
|
||||
modulePath + "/pkg/identity",
|
||||
}
|
||||
|
||||
for _, pkg := range protocolPackages {
|
||||
|
|
@ -97,8 +97,8 @@ func TestProtocolLayerImports(t *testing.T) {
|
|||
continue
|
||||
case allowedExternal(dep):
|
||||
continue
|
||||
case strings.HasPrefix(dep, modulePath+"/internal/address"),
|
||||
strings.HasPrefix(dep, modulePath+"/internal/identity"):
|
||||
case strings.HasPrefix(dep, modulePath+"/pkg/address"),
|
||||
strings.HasPrefix(dep, modulePath+"/pkg/identity"):
|
||||
// Protocol packages may depend on each other.
|
||||
continue
|
||||
case strings.HasPrefix(dep, "internal/"),
|
||||
|
|
@ -123,11 +123,11 @@ func TestProtocolLayerImports(t *testing.T) {
|
|||
// and transport layers cannot contain signing capability even by accident.
|
||||
func TestProtocolDoesNotImportSigner(t *testing.T) {
|
||||
requireToolchain(t)
|
||||
signerPkg := modulePath + "/internal/identity/signer"
|
||||
signerPkg := modulePath + "/pkg/identity/signer"
|
||||
|
||||
for _, pkg := range []string{
|
||||
modulePath + "/internal/address",
|
||||
modulePath + "/internal/identity",
|
||||
modulePath + "/pkg/address",
|
||||
modulePath + "/pkg/identity",
|
||||
} {
|
||||
for _, dep := range goList(t, "-deps", pkg) {
|
||||
if dep == signerPkg {
|
||||
|
|
@ -154,7 +154,7 @@ func TestNoSigningOutsideSigner(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, file := range files {
|
||||
if strings.Contains(file, "/internal/identity/signer/") {
|
||||
if strings.Contains(file, "/pkg/identity/signer/") {
|
||||
continue
|
||||
}
|
||||
data, err := readFile(file)
|
||||
|
|
@ -164,7 +164,7 @@ func TestNoSigningOutsideSigner(t *testing.T) {
|
|||
for _, api := range signingAPIs {
|
||||
if strings.Contains(data, api) {
|
||||
t.Errorf("%s uses %s outside the signer package; "+
|
||||
"private key operations must stay in internal/identity/signer", file, api)
|
||||
"private key operations must stay in pkg/identity/signer", file, api)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,8 +22,8 @@ import (
|
|||
"errors"
|
||||
"io"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -3,8 +3,8 @@ package protocol_test
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
// TestAccessors covers the trivial Value/object accessors that the golden and
|
||||
|
|
@ -3,8 +3,8 @@ package protocol
|
|||
import (
|
||||
"bytes"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
// Decoders for the six protocol objects.
|
||||
|
|
@ -3,8 +3,8 @@ package protocol
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
// Encoders for the six protocol objects.
|
||||
|
|
@ -4,9 +4,9 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
// ExampleClaimLifecycle is the whole client-side story for one claim, with no
|
||||
|
|
@ -8,9 +8,9 @@ import (
|
|||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
// loadVectorsF is loadVectors for the seed-corpus phase of a fuzz test,
|
||||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity/signer"
|
||||
)
|
||||
|
||||
const vectorsPath = "../../testdata/vectors/tce_vectors.json"
|
||||
|
|
@ -81,8 +81,8 @@ func TestAllowedImports(t *testing.T) {
|
|||
"crypto/subtle": true,
|
||||
"errors": true,
|
||||
"fmt": true,
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/address": true,
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce": true,
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/address": true,
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce": true,
|
||||
}
|
||||
for _, f := range scanPackageProtocol(t, ".") {
|
||||
for _, imp := range importsOfProtocol(t, f) {
|
||||
|
|
@ -3,7 +3,7 @@ package protocol_test
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
)
|
||||
|
||||
// Mutation tests: any single-byte change to the canonical bytes or to the
|
||||
|
|
@ -19,7 +19,7 @@ import (
|
|||
"bytes"
|
||||
"errors"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
// Errors returned when an object violates a protocol rule.
|
||||
|
|
@ -4,8 +4,8 @@ import (
|
|||
"errors"
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
// TestRejectVectors runs the frozen malformed-encoding vectors from
|
||||
|
|
@ -22,8 +22,8 @@ import (
|
|||
"bytes"
|
||||
"crypto/subtle"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
// KeyRotationRequest is the incoming key's claim of succession, tag 0x08.
|
||||
|
|
@ -6,8 +6,8 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
// Rule tests for the per-object constraints of PROTOCOL.md section 8. The
|
||||
|
|
@ -5,9 +5,9 @@ import (
|
|||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
// Golden tests against the frozen reference vectors.
|
||||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"crypto/ed25519"
|
||||
"crypto/subtle"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
// Verification of the six protocol objects, following the order of
|
||||
|
|
@ -4,9 +4,9 @@ import (
|
|||
"errors"
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
// Verification tests for PROTOCOL.md sections 7.3 and 8. The signature order
|
||||
|
|
@ -3,7 +3,7 @@ package tce_test
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
func TestIDAccessors(t *testing.T) {
|
||||
|
|
@ -3,7 +3,7 @@ package tce_test
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
// TestCanonicalNumber exercises section 5.1: arbitrary-precision decimals must
|
||||
|
|
@ -3,7 +3,7 @@ package tce_test
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
func TestValueConstructorsAndAccessors(t *testing.T) {
|
||||
|
|
@ -13,8 +13,8 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
// Envelope is the JSON transport form of one signed object.
|
||||
|
|
@ -3,10 +3,10 @@ package transport_test
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/transport"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/transport"
|
||||
)
|
||||
|
||||
func TestEnvelopeViewAndVerify(t *testing.T) {
|
||||
|
|
@ -4,9 +4,9 @@ import (
|
|||
"encoding/hex"
|
||||
"encoding/json"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
)
|
||||
|
||||
func hexStr(b []byte) string { return hex.EncodeToString(b) }
|
||||
|
|
@ -4,12 +4,12 @@ import (
|
|||
"bytes"
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/address"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/verify"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/verify"
|
||||
)
|
||||
|
||||
// delegationFixture builds a graph plus signers for chain scenarios.
|
||||
|
|
@ -4,11 +4,11 @@ import (
|
|||
"bytes"
|
||||
"testing"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/verify"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/verify"
|
||||
)
|
||||
|
||||
// rotationFixture stores both halves of a rotation link.
|
||||
|
|
@ -14,11 +14,11 @@ import (
|
|||
"sort"
|
||||
"sync"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/transport"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/transport"
|
||||
)
|
||||
|
||||
// Graph holds the verified objects under evaluation.
|
||||
|
|
@ -5,13 +5,13 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/transport"
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/verify"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/identity/signer"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/transport"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/verify"
|
||||
|
||||
"git.n1ko.dev/Niko/niko_trust/internal/address"
|
||||
"git.n1ko.dev/Niko/niko_trust/pkg/address"
|
||||
)
|
||||
|
||||
const base = uint64(1_700_000_000)
|
||||
BIN
service
Executable file
BIN
service
Executable file
Binary file not shown.
Loading…
Add table
Reference in a new issue