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:
Niko Marmeladkov 2026-08-26 12:49:54 +03:00
parent 79df689f7a
commit 3bf13fa488
82 changed files with 797 additions and 110 deletions

View file

@ -0,0 +1 @@
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABQEg45x7MvYhHfhpEJeFV1ftnO/Oorhy/5W5sX5YYLSVStoAINBKsjJ0K7SrOhNovUYV5ObQIkq3GgFrr4UgozLJd4c3AfzsutQGECa+hvDDQGf4726neI5ispE=","signature":"zGL6tBvs5fO2Neur/nwV5AjhZGNquheAdtqTahBMvt44IekpPUuXdHqaiDNu/aPhClO7C4ijVlxxj98iY9MYAg==","object_id":"0e972deae436609ece1bf2aa80232fb6cf1b386bef5ed59cc0ee2abbf3de5cc7"}

View file

@ -0,0 +1 @@
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABAEAIOcA8DdWUiDbQo+nTu3ef0WKzCqMGhiXMYY2Sf5TT/koACCRvis7oAKfVr+F4lfOGDXOzB/0mI1pmhUeVjYrhlxaCARhdXRoAAx3cyBsaXZlIHRlc3SApKfaBrykp9oGEAcHBwcHBwcHBwcHBwcHBwc=","signature":"lnBnOW9Q5VuOtcCnxi1JXtRKV9z4Q86IouxwASclUzLRTdIsJ0evxq+/dBmIdt/6Hdxr5213UIGQVVn5HMkaCw==","object_id":"102ae6a0161833426efb7483cf7a2ff3c30f52b8c865520a136530566c33f823"}

View file

@ -0,0 +1 @@
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABAEAIILLyye2RTa7ii04SOM8iyDZw+veAHiwLX9LNytKirMKACDQSrIydCu0qzoTaL1GFeTm0CJKtxoBa6+FIKMyyXeHNwVsb2dpbgEHc2Vzc2lvbgMWS2pfWThqNVFCaUNFRmREUWd6QUtyQRdTaWduIGluIHRvIGRlbW8gc2VydmljZfbnutQGsui61AYQKB2UGcgF4ZNwWHdSmG0Etw==","signature":"3agPqlkbvKBpcIKf2fCz1QJxDPzLghlT583J/HTvcbpJesNkLtLWBmqoVopoqjW1qZcAyVkmShVSGYa1NTsKAA==","object_id":"1ab38c6aa921fb5eaaba099479341811e37afd7273d09d2e39c90192c532ee4c"}

View file

@ -0,0 +1 @@
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABAEAILOiOd/2wrH9Xd8KeVuTO6jprESVzIQdrry1RhALhETeACB/2ppLTnSq9hP6ZRY4K4olAOg94xHdULPB24QQh/PWvwRhdXRoABRhbmRyb2lkIHdzIGxpdmUgdGVzdICkp9oGvKSn2gYQBwcHBwcHBwcHBwcHBwcHBw==","signature":"0A62EzU0s8+MfWa+GPyd+J80xprG4orCajBD2rnupU5uet41luCCzFR3cB+TtzlE+Rrh+DrBq4tI3kpCLUUbAA==","object_id":"2745e5cba6f8c208f58e3eaac50c07ed871bd3977223767c1f8adf831467bbc7"}

View file

@ -0,0 +1 @@
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABAEAIILLyye2RTa7ii04SOM8iyDZw+veAHiwLX9LNytKirMKACDQSrIydCu0qzoTaL1GFeTm0CJKtxoBa6+FIKMyyXeHNwVsb2dpbgEHc2Vzc2lvbgMWbzY0TFpZemZROFJWUmNKUE83OUhtQRdTaWduIGluIHRvIGRlbW8gc2VydmljZe3nutQGqei61AYQY5R39BPmNKy6H7eeqdvN3w==","signature":"YOnrIHdzsIKyHrHZsNcQtSfiaKEvD6UchTOcvMOXZQm0GdJ4rEN2I71fhVpv/JP2Gt3sisuy/7MrrxFrd51AAw==","object_id":"618ff3cb6713ba3aecab174ca3037f714b90f5a2094323e30a94ee5abacb77bc"}

View file

@ -0,0 +1 @@
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABQEgyG2kH+/FGwQZKum7uHeiaHlM5CUUEiAu/crilN27sRcAINBKsjJ0K7SrOhNovUYV5ObQIkq3GgFrr4UgozLJd4c3Af/nutQGEKrodzQXiRYoNh/K6DU7x9o=","signature":"ba3ZEGaovxTLixjkP9KiDIrJ1zHGllW6mNgA3ORF/19m/M30XgaiJXbmCKtkQshi9E4xGD5+U/dmcAmPDJO7Aw==","object_id":"7d59b6aba3a2681f90c5c705f3fb0ab1c53323326839408d4dd351580a8a1972"}

View file

@ -0,0 +1 @@
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABQEgYY/zy2cTujrsqxdMowN/cUuQ9aIJQyPjCpTuWrrLd7wAINBKsjJ0K7SrOhNovUYV5ObQIkq3GgFrr4UgozLJd4c3Ae7nutQGEC3VyacYr+5LBo0xsHtvo1Y=","signature":"czfLYKfIMlzRXr3nDa+q934ciyYA00+nL+uA0pSRN+nFNHBUo/S5sFjPehP9oHYh0VUeklbObckrerdW5BtvAg==","object_id":"816d5d2e26f12f4f4ea339d53e8dcc36478e73bb5ecce03148c57890b44668ec"}

View file

@ -0,0 +1 @@
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEAAQEAIGgxl6UgyEP7hHyaXK+FCuY7aZMx2iIc0oJcWza8bVR2CHBvdy1saXZlgOLPqgY=","signature":"vu1W9QFDcH7R/KunZbzoJMwhm0on+UnqoqpyDd+W8AOV6gDg6L6dlTRdY6Ge/0OSl+lXufW+xgiRChrsh5y0CA==","object_id":"94764932c222671278b579b5cf8dbce706f71c023f9a11c32a61bf307bceab56"}

View file

@ -0,0 +1 @@
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEAAQEAIIJEncifJ40xeCa/nFA2Zy/eIBTNS8P7/s2R6pNesc6YCHBvdy1saXZlgOLPqgY=","signature":"WjY2lHununQjG0NOkNPzzyStsr3IxdiOToLNRlxP2YsuRUXpc0fvKho71H12hO+/3GYHZgKkP7IQvEWmD8NpAA==","object_id":"a902de693d1ea14571eb1786d3f7e545b7bb4e8e81226d00df2548972ebd4e85"}

View file

@ -0,0 +1 @@
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABAEAIILLyye2RTa7ii04SOM8iyDZw+veAHiwLX9LNytKirMKACCRvis7oAKfVr+F4lfOGDXOzB/0mI1pmhUeVjYrhlxaCAVsb2dpbgEHc2Vzc2lvbgMWTDlCTm9YaEVwcWd6bnhOUFl1UUhyQRdTaWduIGluIHRvIGRlbW8gc2VydmljZc3nutQGiei61AYQMbmTvcU0ct/YQH5qLSU48w==","signature":"mJigLGS2gykFOEQ/hvfo40Kca79ZNIgv8GtY2bWKU9gjJ+pjpjd3TgGd5R/ss++QNoROBcIqP/Ndpgiw5WX9Bw==","object_id":"c4a41dcadab39ba3b10d2f7749338e5c5f8c2ea4e86b589ff3067d28204ca153"}

View file

@ -0,0 +1 @@
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABAEAIILLyye2RTa7ii04SOM8iyDZw+veAHiwLX9LNytKirMKACDQSrIydCu0qzoTaL1GFeTm0CJKtxoBa6+FIKMyyXeHNwVsb2dpbgEHc2Vzc2lvbgMWeS04ak10enVvTnBUelluLVpmbmRPURdTaWduIGluIHRvIGRlbW8gc2VydmljZf/nutQGu+i61AYQZGJ4M7FvkiCnoTb/TFSwtw==","signature":"wVgK0dZ9qSBhimQzELmfdd52DYrK3nwP6Rv+ZNDHgrSKI4Kapww07O0E8BNRVhXagrZdMPtpjbSNJDEAhhDLDg==","object_id":"c86da41fefc51b04192ae9bbb877a268794ce4251412202efdcae294ddbbb117"}

1
data/checkpoint-1.json Normal file
View file

@ -0,0 +1 @@
{"bytes":"dHJ1c3QubjFrby5kZXYvY2twdC8xAAEBASCa4Wwr3EzcvPeg2HpkwiKrsJdV9xapyWTyt1Iuis+NpSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAODst9QG","signature":"cANWA+qCI4aANYhUpsN0cwgtnOb8sgGBcphmydAG/XJYQnWCbOtRLeInsf6cHxt64Oms7962/Zvdgs4mjxn/Ag==","id":"29080de9e64a75de4453a96f5c4294bcc3b3d81c4402a664ebb3a2f94e657c33"}

1
data/checkpoint-2.json Normal file
View file

@ -0,0 +1 @@
{"bytes":"dHJ1c3QubjFrby5kZXYvY2twdC8xAAECAiAO+gsIekCmIn1LaLOZVZdaK2JWBBKupWpvU4CIAkLEayApCA3p5kp13kRTqW9cQpS8w7PYHEQCpmTrs6L5TmV8M7f0t9QG","signature":"Rx9wLAo5NFvv3NNuOzXyB3MwUWU3u3/uLHKlHlraz+bxCVVI+So6Cvh5MVNvjBDf92Sg6Np+5Lb2NMZaNp7JDQ==","id":"6ae384a485308701adf6f0da2767266ff90f0c25b024ba706fdd1c60ca010f6c"}

View file

@ -0,0 +1 @@
{"epoch":2,"last_hash":"6ae384a485308701adf6f0da2767266ff90f0c25b024ba706fdd1c60ca010f6c"}

View file

@ -0,0 +1 @@
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABAEAIPMtJyRnRoKrwG4/7YYYNg+Q/X358s7n7K4cPkcz3PtjACDVXkf9iQ8Zx+mWZW8YMFy/f/Xk6TpYsr5C0v8ldWigUwRhdXRoAAx3cyBsaXZlIHRlc3SApKfaBrykp9oGEAcHBwcHBwcHBwcHBwcHBwc=","signature":"EF6/T3cGoF4HJ47yGM9QfMB1eilnvbrSg8vtqupLF2f+zOO1fflYKmQMXBi8RJyrX/pJTMBdZspH3sJkQcFiAw==","object_id":"d8a462d2755ffb16d8bc615e78db232289f571988b158febf190f71a969153d8"}

View file

@ -0,0 +1 @@
{"tce":"dHJ1c3QubjFrby5kZXYvdGNlLzEABAEAILZvMfVh6H5uBBpoA570y5ZKs5TXNc6U5U0ff9wVJfltACDQSrIydCu0qzoTaL1GFeTm0CJKtxoBa6+FIKMyyXeHNwVsb2dpbgEHc2Vzc2lvbgMWVHBqTDNFZkdhS20wczZ0TXpQQTU4ZxdTaWduIGluIHRvIGRlbW8gc2VydmljZfzsutQGuO261AYQA8GUwWqDoGyqy9Y0E83RhQ==","signature":"wG7vho7wxIMqO7205aFzbhAtpkHvRSHhrLvs+iKYyk51MNZZZgCOF4LFk2hFv3SiOkxRnm5rcWwgjyqGK5AcDA==","object_id":"e39c7b32f6211df8691097855757ed9cefcea2b872ff95b9b17e5860b4954ada"}

2
data/relay_key.seed Normal file
View 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
View 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
View 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
View 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
}

View file

@ -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.

View file

@ -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())
}
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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.

View file

@ -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 (

View file

@ -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 {

View file

@ -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.

View file

@ -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) {

View file

@ -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

View file

@ -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

View file

@ -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) {

View file

@ -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

View file

@ -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.

View file

@ -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 {

View file

@ -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)
}
}
}

View file

@ -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 (

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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,

View file

@ -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"

View file

@ -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) {

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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) {

View file

@ -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

View file

@ -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) {

View file

@ -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.

View file

@ -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) {

View file

@ -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) }

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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

Binary file not shown.