Pakiety dowodowe: podpisany zapis zdarzenia dotyczącego zaufania do GNSS
Pakiet jest zapisem technicznym, a nie opinią prawną. Podaje, co silnik Kshana obliczył z podanego dziennika odbiornika przy podanej konfiguracji, i pozwala każdemu sprawdzić, że nic w nim nie zmieniono później. Nie mówi, co spowodowało zdarzenie, kto za nie odpowiadał, czy spełniono jakikolwiek obowiązek ani że dziennik pokazuje to, co odbiornik faktycznie odebrał. Mówią o tym zarówno manifest, jak i podsumowanie.
Pakiet dowodowy służy do raportu z incydentu, dla ubezpieczyciela lub na pytanie regulatora: „co wasz system powiedział o sygnale GNSS między tymi dwoma chwilami i czy możemy ufać temu zapisowi?”
Utwórz pakiet#
# once: a signing key. The private key stays with you; never commit it.
kshana evidence keygen --out signer.key # writes signer.key (0600) and signer.key.pub
kshana receiver-trust evidence session.toml \
--from 100 --to 260 \ # seconds since the first epoch, or ISO-8601 UTC
--key signer.key --out pack/ \
--title "Berth 4, 14 June" # optional
Dla pakietu utworzonego z sesji laboratoryjnej --lab-attestation lab.toml zapisuje w podpisanym manifeście oświadczenie operatora o układzie (przewodowy lub ekranowany, tłumienie lub obudowa, co emitowało jakikolwiek sygnał RF); zob. HARDWARE-SAFETY.md. Jest zapisywane tak, jak podano, i nigdy nie jest weryfikowane.
--from i --to to sekundy od pierwszej epoki dziennika albo czas UTC w formacie ISO-8601, gdy dziennik podaje własny czas początku. --out nie może już zawierać plików. --created-utc przyjmuje rzeczywisty czas UTC, taki jak 2026-01-02T03:04:05Z (cokolwiek innego jest odrzucane), lub none, co pomija czas utworzenia i czyni pakiet odtwarzalnym bajt w bajt. Czas utworzenia jest tym, co poda podpisujący; nic nie sprawdza go względem zegara, do czego służy opcjonalny token znacznika czasu.
Plik klucza zawiera 32-bajtowe ziarno (seed) Ed25519 jako 64 cyfr szesnastkowych; podaj własny (na przykład przechowywany w twoim systemie zarządzania kluczami) albo pozwól, by keygen go utworzył. kshana ostrzega, jeśli plik jest czytelny dla innych użytkowników. .gitignore wyklucza *.evidence-key; nazwij tak swój klucz, jeśli leży w kopii roboczej repozytorium.
Co jest w pakiecie#
| Plik | Zawartość |
|---|---|
log-slice.bin |
surowe bajty dziennika odbiornika dla okna |
config.json |
scenariusz tak, jak go uruchomiono (każdy próg monitora, zdarzenie i reguła porównania), uruchomione monitory, punkt odniesienia kalibracji i skrót scenariusza; dla statku także oświadczenie doradcze, które niesie każdy wynik dla statku (oprogramowanie doradcze, nie jest to sprzęt nawigacyjny z zatwierdzeniem typu); od 0,36 także geometry_model (faktycznie użyta tabela [geometry], waga monitora geometrii, każde pochodzenie i reguły klas). Nazwy katalogów i dane wbudowane są usuwane. |
epochs.json |
każda epoka w oknie ze statystykami silnika, alarmami (przyczynami) i stanem zaufania; od 0,36 także jej satelitarne geometry (dobre, pogorszone z przyczynami lub nieznane, gdy dziennik nie podaje geometrii satelitów) oraz class przy każdym wniosku (geometry, environment, inconsistency) |
summary.html |
samodzielne podsumowanie bez skryptów: identyfikacja, skróty, wyniki, przyczyny, pas geometrii (epoki według stanu geometrii) oddzielony od klas wniosków (0,36), każdy próg, tabela dla poszczególnych epok oraz ograniczenia zapisu |
manifest.json |
wersja silnika, okno, czas utworzenia, format i nazwa dziennika, SHA-256 pełnego dziennika, zakres i SHA-256 wycinka, klucz publiczny i odcisk podpisującego, rozmiar i SHA-256 każdego pliku oraz łańcuch skrótów po plikach |
manifest.sig |
podpis Ed25519 po dokładnych bajtach manifest.json |
timestamp.tsr |
opcjonalny token znacznika czasu RFC 3161 |
Wycinek dziennika. Czytnik podaje zakres bajtów źródłowych każdej epoki, więc wycinek jest najmniejszym zakresem bajtów, który zawiera każdą epokę z okna (byte-range): NMEA, u-blox UBX, obserwacje RINEX 3 i dzienniki Androida. Jest haszowany jako surowe bajty, nigdy jako zdekodowany tekst. Dla kanału na żywo lub RINEX 2 zakresu nie ma; wtedy dołącza się cały dziennik, a manifest podaje whole-log (okno ogranicza, które epoki są raportowane, a nie które bajty są dołączone). Podsumowanie podaje, który przypadek zachodzi. Wycinek jest zapisem bajtów okna, a nie samodzielnym dziennikiem: na przykład wycinek RINEX nie ma nagłówka pliku.
Łańcuch skrótów. Manifest wymienia pliki w ustalonej kolejności. link₀ = SHA-256("kshana- evidence-chain/1"); dla każdego pliku linkᵢ = SHA-256(linkᵢ₋₁ ‖ SHA-256(file) ‖ len(name) ‖ name) (długość jako 4 bajty, big-endian). Ostatnie ogniwo to chain_head. Zmiana kolejności, usunięcie lub podmiana pliku zmienia łańcuch; podpis obejmuje łańcuch.
Zweryfikuj pakiet#
kshana evidence verify pack/ --pubkey signer.key.pub [--log session.nmea] \
[--require-timestamp] [--allow-unpinned] [--json]
Kod wyjścia 0 oznacza zweryfikowano; 1 oznacza, że coś zawiodło; 2 oznacza błąd użycia; 3 oznacza, że pakiet jest nienaruszony, ale jego podpisujący nie był przypięty (zob. niżej). Każda kontrola zgłasza wynik pozytywny, negatywny lub pominięcie, a każda porażka jest nazwana:
| Kod porażki | Znaczenie |
|---|---|
manifest-missing, manifest-malformed, unsupported-format |
nie można odczytać manifestu |
signature-missing, signature-malformed, signature-invalid |
brak podpisu, to nie jest 64-bajtowy podpis szesnastkowy małymi literami albo nie weryfikuje się po manifest.json |
public-key-malformed, public-key-mismatch |
klucz jest nieprawidłowy lub różni się od podanego przez ciebie |
file-missing, file-size-mismatch, file-hash-mismatch |
wymieniony plik jest nieobecny lub różni się od manifestu; plik jest wskazany z nazwy |
unlisted-file |
plik jest w pakiecie, ale nie ma go w manifeście |
chain-mismatch, chain-head-mismatch |
ogniwo nie jest tym, co dają jego dane wejściowe; plik jest wskazany z nazwy |
artifact-list-malformed, slice-record-inconsistent, epoch-count-mismatch |
manifest zaprzecza sam sobie lub epochs.json |
full-log-hash-mismatch, slice-not-from-full-log |
z --log: dziennik, który masz, nie jest tym zapisanym albo wycinek nie jest tym zakresem dziennika |
timestamp-missing, timestamp-malformed, timestamp-imprint-mismatch |
zob. poniżej |
Zmiana jednego bajtu w dowolnym pliku powoduje porażkę weryfikacji z odpowiednim powodem: zmieniony artefakt jest zgłaszany jako niezgodność skrótu tego pliku i nic więcej; zmieniony manifest powoduje porażkę podpisu. Ponowne obliczenie skrótów i łańcucha po edycji pliku nie pomaga bez klucza podpisującego; wtedy zawodzi tylko podpis.
Przypnij klucz. Bez --pubkey pakiet dowodzi jedynie, że jest nienaruszony względem klucza, który sam wskazuje i który każdy może wygenerować. Dlatego weryfikacja kończy się wtedy kodem 3 i mówi „INTACT, BUT THE SIGNER IS NOT PINNED”, a nie „VERIFIED”; --allow-unpinned przyjmuje własny klucz pakietu świadomie i wypisuje „VERIFIED against the key the pack names itself (signer NOT pinned; pass --pubkey)”. Klucz publiczny podpisującego uzyskaj od podpisującego drogą, której ufasz (nie z pakietu) i przekaż go przez --pubkey: 64 cyfr szesnastkowych lub plik .pub. Przekazanie klucza prywatnego przez pomyłkę jest rozpoznawane i odrzucane. Odcisk (pierwsze 128 bitów SHA-256 klucza, 32 cyfry szesnastkowe) jest drukowany do porównania na oko; niezawodną drogą jest przypięcie pełnego klucza.
Format pakietu i procedura weryfikacji (normatywne)#
Ta sekcja jest specyfikacją, z której pisze się niezależny weryfikator. Słowa pisane wielkimi literami są wymaganiami. Pakiet jest katalogiem (lub dowolnym odwzorowaniem nazwy pliku na bajty) plików nazwanych dokładnie tak, jak niżej; weryfikator pracuje na bajtach każdego pliku w postaci zapisanej i niczego nie serializuje ponownie.
Pliki. Pliki artefaktów, w tej ustalonej kolejności, to log-slice.bin, config.json, epochs.json, summary.html. Pakiet ma także manifest.json i manifest.sig i MAY mieć timestamp.tsr. Każda inna nazwa to plik spoza listy.
Szesnastkowe. Każdy ciąg szesnastkowy to wyłącznie małe litery 0-9a-f, bez białych znaków; wielkie litery nie są akceptowane. Wartości SHA-256 mają 64 cyfry, klucz publiczny ma 64 cyfry, podpis ma 128 cyfr.
manifest.sig. 128-cyfrowy zapis szesnastkowy małymi literami 64-bajtowego podpisu Ed25519 (RFC 8032, czysty schemat Ed25519) po dokładnych bajtach manifest.json, po którym następuje dokładnie jeden \n i nic więcej. Podpis MUST być sprawdzany ściśle: podpis, którego wartość S nie jest mniejsza od rzędu grupy L, jest nieprawidłowy.
manifest.json. Obiekt JSON w UTF-8. Jego składowe (weryfikator MUST znaleźć każdą z podanym typem JSON; dodatkowe składowe są ignorowane):
| składowa | typ | znaczenie |
|---|---|---|
format |
ciąg znaków | MUST równać się kshana-evidence/1 |
title, engine_version, disclaimer |
ciąg znaków | opisowe |
created_utc |
ciąg znaków lub null | czas utworzenia, opisowy |
window |
obiekt {from_s, to_s} |
liczby, sekundy |
epochs_in_window |
nieujemna liczba całkowita | liczba elementów tablicy JSON w epochs.json |
log |
obiekt | format ciąg znaków, file_name ciąg znaków, full_sha256 szesnastkowe, full_bytes nieujemna liczba całkowita, start_label ciąg znaków lub null oraz slice |
log.slice |
obiekt | kind (dokładnie byte-range lub whole-log; każda inna wartość czyni manifest wadliwym), start i end nieujemne liczby całkowite (przesunięcia w bajtach, koniec wyłączony), sha256 szesnastkowe |
signer |
obiekt | algorithm ciąg znaków (ed25519), public_key szesnastkowe (32 bajty), fingerprint ciąg znaków |
artifacts |
tablica obiektów | po jednym na plik artefaktu, w ustalonej kolejności: name ciąg znaków, role ciąg znaków, bytes nieujemna liczba całkowita, sha256 szesnastkowe, link szesnastkowe |
chain_head |
szesnastkowe | ostatnie ogniwo |
lab_attestation |
obiekt, opcjonalny | obecny tylko w pakiecie utworzonym z sesji laboratoryjnej (--lab-attestation): oświadczenie operatora operator, organisation, date, setup (conducted lub shielded), attenuation_db lub enclosure, emitter, statement i opcjonalnie signature, zapisane tak, jak podano. Obejmuje je podpis manifestu i nigdy nie jest weryfikowany; weryfikator zgłasza go jako „attested by the named operator; not verified by Kshana”. W pozostałych przypadkach nieobecny, a pakiet bez niego pozostaje bez zmian |
signer.fingerprint to pierwsze 32 cyfry szesnastkowe SHA-256 po 32 bajtach klucza publicznego.
Łańcuch skrótów. link0 to SHA-256 z 23 bajtów ASCII kshana-evidence-chain/1. Dla każdego artefaktu w kolejności: link = SHA-256( previous_link (32 bytes) ‖ SHA-256(file) (32 bytes) ‖ len(name) as 4 bytes big-endian ‖ name as UTF-8 ), gdzie SHA-256(file) bierze się z zapisanego w manifeście sha256 dla tego artefaktu (więc łańcuch jest własnością manifestu i sprawdza się go względem manifestu, a nie względem plików).
Procedura. Weryfikator wykonuje te kroki po kolei i zbiera każdą porażkę; wynikiem jest zbiór porażek, z których każda to code i, gdzie podano, plik name. MUST NOT zatrzymywać się na pierwszej porażce, poza miejscami, gdzie podano. pin to klucz publiczny, któremu weryfikator kazano zaufać, jeśli jakiś jest.
- Brak
manifest.json: porażkamanifest-missing; stop. - Jeśli
manifest.jsonnie jest obiektem JSON ze wszystkimi powyższymi składowymi w podanych typach (w tym wpisamiartifactsz każdą składową orazlog.slice): porażkamanifest-malformed. W przeciwnym razie, jeśliformatnie jest powyższą wartością:unsupported-format. - Faza podpisu. Ta faza jest wykonywana niezależnie od tego, czy manifest dało się odczytać. Jeśli manifest dało się odczytać, klucz wskazany to
signer.public_key; jeśli nie jest to 64 cyfr szesnastkowych małymi literami, porażkapublic-key-malformed(brak klucza wskazanego); w przeciwnym razie, jeślisigner.fingerprintnie jest jego odciskiem, porażkapublic-key-malformed(klucz pozostaje użyteczny). Jeśli istnieją zarównopin, jak i klucz wskazany i się różnią: porażkapublic-key-mismatch. Klucz kontrolny topin, jeśli go podano, w przeciwnym razie klucz wskazany, jeśli istnieje; jeśli nie ma żadnego, nie ma klucza kontrolnego (dotyczy to każdego przypadku, gdy manifestu nie dało się odczytać i nie podanopin). Jeśli nie mamanifest.sig:signature-missing. W przeciwnym razie, jeśli plik nie składa się dokładnie z 128 cyfr szesnastkowych małymi literami plus jednego\n:signature-malformed. W przeciwnym razie, jeśli jest klucz kontrolny, który nie jest prawidłowym kodowaniem klucza publicznego Ed25519:public-key-malformed. W przeciwnym razie, jeśli jest klucz kontrolny, a podpis nie weryfikuje się ściśle po bajtachmanifest.json(jakiekolwiek by były):signature-invalid. (Bez klucza kontrolnego podpis nie jest sprawdzany i nie zgłasza się z tego powodu żadnej porażki.) - Jeśli manifestu nie dało się odczytać (krok 2 dał
manifest-malformed): zatrzymaj się tutaj. - Artefakty. Jeśli nazwy
artifactsnie są dokładnie czterema powyższymi w kolejności:artifact-list-malformed. Następnie dla każdego wpisu manifestu, w kolejności: jeśli pakiet nie ma pliku o tej nazwie,file-missing(nazwa); w przeciwnym razie, jeśli jego długość różni się odbytes,file-size-mismatch(nazwa); i niezależnie, jeśli SHA-256 pliku różni się odsha256,file-hash-mismatch(nazwa). - Pliki spoza listy: dla każdego pliku w pakiecie innego niż
manifest.json,manifest.sig,timestamp.tsri nazwy zartifactsmanifestu:unlisted-file(nazwa). - Łańcuch. Przejdź po
artifactsmanifestu w kolejności odlink0. Jeślisha256wpisu nie jest 64 cyframi szesnastkowymi małymi literami:manifest-malformedi łańcuch jest porzucany (bez sprawdzania głowy). W przeciwnym razie oblicz ogniwo; jeśli różni się odlinkwpisu:chain-mismatch(nazwa). Po przejściu, jeśli łańcuch nie został porzucony, a ostatnie ogniwo różni się odchain_head:chain-head-mismatch. - Wycinek i epoki. Zgłoś
slice-record-inconsistent(co najwyżej raz na powód, a weryfikator zgłasza kod raz niezależnie od liczby zachodzących powodów), jeśli zachodzi którekolwiek z:start > end;end > full_bytes;sha256wpisulog-slice.binróżni się odslice.sha256;bytestego wpisu różni się odend - start; albokindtowhole-logi zachodzi którekolwiek zstart != 0,end != full_bytes,slice.sha256 != full_sha256. Jeśliepochs.jsonistnieje i parsuje się jako tablica JSON, której długość różni się odepochs_in_window:epoch-count-mismatch. - Pełny dziennik. Tylko jeśli weryfikatorowi podano oryginalne bajty dziennika: jeśli ich SHA-256 różni się od
log.full_sha256,full-log-hash-mismatch; w przeciwnym razie, jeśli zakresstart..endnie mieści się w dzienniku albo bajty dziennika w tym miejscu różnią się odlog-slice.bin,slice-not-from-full-log. - Znacznik czasu. Bez
timestamp.tsr: jeśli weryfikatorowi kazano go wymagać,timestamp-missing; w przeciwnym razie nic. Z nim: jeśli nie da się go odczytać jako tokenu lub odpowiedzi RFC 3161 (zob. niżej),timestamp-malformed; w przeciwnym razie, jeśli jego odcisk komunikatu nie jest skrótem, w algorytmie własnym odcisku (SHA-256, SHA-384 lub SHA-512), bajtówmanifest.json:timestamp-imprint-mismatch.
Pakiet jest zweryfikowany dokładnie wtedy, gdy zbiór porażek jest pusty. To, czy podpisujący był przypięty, jest zgłaszane osobno i nie zmienia zbioru porażek.
Token znacznika czasu. Token to DER ContentInfo, którego typ zawartości to CMS SignedData (1.2.840.113549.1.7.2), a typ zawartości zamkniętej to id-ct-TSTInfo (1.2.840.113549.1.9.16.1.4); odpowiedź to DER TimeStampResp (PKIStatusInfo po którym następuje token), której status to 0 lub 1. Algorytm odcisku komunikatu w TSTInfo musi być SHA-256, SHA-384 lub SHA-512 ze skrótem długości tego algorytmu, a jego genTime musi być rzeczywistym czasem kalendarzowym UTC. BER o nieokreślonej długości nie jest akceptowany. Podpis organu i łańcuch certyfikatów są sprawdzane tylko wtedy, gdy podano zaufane kotwice (--tsa-ca, niżej).
Etykieta dowodowa (0,40)#
Pakiet wykonany przez 0,40 lub nowszą wersję niesie, wewnątrz config.json, wpis evidence (poziom, VALIDATED, MODELLED lub PARTNER, czyli najsłabszy status wśród jego wierszy, oraz identyfikatory i wymagania wierszy macierzy weryfikacji, na których opiera się zawartość pakietu: wiersz integralności pakietu, wiersze receiver-trust oraz wiersze jammertest-scoring lub pełnej pętli dla tych pakietów) i config_version "1.1". Jest wewnątrz config.json, więc skrót pliku manifestu, łańcuch skrótów i podpis go obejmują: jego zmiana powoduje niepowodzenie weryfikacji. kshana evidence verify podaje poziom i identyfikatory wierszy.
Zmiana jest addytywna i tylko w przód. format manifestu pozostaje kshana-evidence/1: normatywny tekst powyżej mówi, że weryfikator MUSI znaleźć tę wartość, i weryfikator czystego pomieszczenia w scripts/ ją znajduje. Wersja podrzędna jest więc niesiona przez config_version (nieobecne w pakiecie wykonanym przed 0,40, co oznacza 1,0), a nie przez nowy ciąg format. Pakiet wykonany przed 0,40 nie ma etykiety i weryfikuje się bez zmian; kshana evidence verify mówi „no evidence label (pre-0.40 pack)”. Testy to tests/evidence_pack_label.rs (w tym zatwierdzone pakiety wzorcowe sprzed 0.40) i tests/evidence_labels_doc_sync.rs.
Znacznik czasu RFC 3161 (opcjonalny)#
Token znacznika czasu od organu znakowania czasem pokazuje, że manifest istniał w chwili, którą organ poświadcza. Token dotyczy manifest.json, więc żąda się go po utworzeniu pakietu (nie da się go dostarczyć przy tworzeniu pakietu, a create_bundle odrzuca token, który nie dotyczy dokładnie tego manifestu):
openssl ts -query -data pack/manifest.json -sha256 -cert -out req.tsq
# send req.tsq to your authority, get the response, then:
kshana evidence attach-timestamp pack/ response.tsr
openssl ts -verify -in response.tsr -data pack/manifest.json -CAfile authority-ca.pem
kshana evidence verify odczytuje token (sam token lub pełną odpowiedź), sprawdza, że został przyznany i że jego odcisk komunikatu równa się skrótowi manifest.json, i wypisuje podany w nim czas. Bez zaufanych kotwic dodaje „timestamp authority signature not verified by Kshana” i zgłasza authority_signature_verified: false; tę część sprawdza wiersz openssl ts -verify powyżej. Token, którego odcisk się różni, lub taki, który się nie parsuje, jest porażką. Typy zawartości tokenu, długość odcisku i czas wygenerowania są sprawdzane pod kątem sensowności.
Z --tsa-ca authority-ca.pem (jeden lub więcej certyfikatów PEM, wszystkie zaufane) verify sprawdza także organ, offline, tak jak robi to openssl ts -verify: skrót komunikatu w podpisanych atrybutach, podpis (RSA PKCS#1 v1.5, RSA-PSS, ECDSA P-256 lub P-384), powiązanie certyfikatu podpisującego ESS, krytyczne rozszerzone użycie klucza timeStamping podpisującego oraz łańcuch do samowystawionej kotwicy, przy czym każdy certyfikat jest ważny w genTime tokenu. Porażka jest zgłaszana jako timestamp-authority-invalid z pierwszą kontrolą, która zawiodła, a authority_signature_verified jest prawdą tylko wtedy, gdy wszystko przeszło. Unieważnienia (CRL lub OCSP) nie są sprawdzane. Ten weryfikator zgadza się z OpenSSL 3.0.13 w 27 samodzielnie wygenerowanych przypadkach (tests/fixtures/evidence_tsa/PREREGISTRATION.md).
Token leży obok podpisanego manifestu, a nie w nim. Nic w podpisie ani w łańcuchu go nie obejmuje, więc token można usunąć albo podmienić na inny ważny token, bez porażki podpisu. verify dlatego zauważa, gdy pakiet go nie ma, a --require-timestamp czyni brak tokenu porażką (timestamp-missing); podmieniony token jest wychwytywany tylko wtedy, gdy nie pasuje do manifestu. attach-timestamp odmawia nadpisania istniejącego tokenu, chyba że podasz --replace. Jeśli znacznik czasu ma dla ciebie znaczenie, przechowuj token i wynik weryfikacji we własnych zapisach i go wymagaj.
Z kodu#
kshana::evidence::build_receiver_trust_pack to jedyny punkt wejścia, który wywołuje wiersz poleceń i każda inna powierzchnia, by utworzyć pakiet: na wejściu scenariusz, bajty dziennika (i pliku nawigacyjnego dla RINEX), okno jako tekst, tytuł i opcjonalny czas utworzenia oraz ziarno (seed) podpisu; na wyjściu pliki, liczba epok, wycinek, skrót manifestu i odcisk podpisującego. Nie czyta plików ani zegara, więc te same dane wejściowe dają te same bajty. Poniżej create_bundle i verify_bundle są funkcjami czystymi: na wejściu bajty (EvidenceInput, 32-bajtowe ziarno, BTreeMap<String, Vec<u8>> plików), na wyjściu wyniki możliwe do serializacji (Files, VerifyReport). Nie używają plików, zegara ani stanu procesu i budują się dla wasm32, więc powierzchnie Pythona, WebAssembly i serwera wywołują je bez zmian. Wiersz poleceń jest cienką nakładką na nie.
Czego pakiet nie robi#
- Nie powtarza oceny. Każdy, kto ma pełny dziennik i
config.json, może ponownie uruchomićkshana receiver-trusti porównać zepochs.json; pakiet dowodzi, co zapisano, a nie że silnik się nie mylił. - Nie dowodzi, że dziennik jest autentyczny ani kompletny. Skrót pełnego dziennika wiąże zapis z jednym plikiem; skąd ten plik pochodzi, musi ustalić operator.
- Nie niesie żadnego twierdzenia o skuteczności wykrywania ani odrzucania zakłóceń.
- Jest zapisem technicznym, a nie opinią prawną ani certyfikacją.