Pacotes de evidências: um registro assinado de um evento de confiança do GNSS
Um pacote é um registro técnico, não um parecer jurídico. Ele declara o que o motor do Kshana calculou a partir de um log de receptor declarado, sob uma configuração declarada, e permite que qualquer pessoa verifique que nada nele foi alterado depois. Ele não diz o que causou um evento, quem foi responsável, se alguma obrigação foi atendida, nem que o log mostra o que o receptor de fato recebeu. O manifesto e o resumo dizem isso.
Um pacote de evidências serve para um relatório de incidente, uma seguradora ou uma pergunta de um regulador: "o que o seu sistema disse sobre o sinal GNSS entre estes dois instantes, e podemos confiar nesse registro?"
Gerar um pacote#
# 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
Para um pacote gerado a partir de uma sessão de laboratório, --lab-attestation lab.toml registra no manifesto assinado a declaração do operador sobre a montagem (conduzida ou blindada, atenuação ou invólucro, o que emitiu qualquer RF); veja HARDWARE-SAFETY.md. Ela é registrada como foi dada e nunca é verificada.
--from e --to são segundos desde a primeira época do log, ou um horário UTC ISO-8601 quando o log informa o próprio horário de início. --out não pode já conter arquivos. --created-utc aceita um horário UTC real, como 2026-01-02T03:04:05Z (qualquer outra coisa é recusada) ou none, que omite o horário de criação e torna o pacote reproduzível byte a byte. O horário de criação é o que o signatário declara; nada o confronta com um relógio, e é para isso que serve o token de carimbo de tempo opcional.
O arquivo de chave contém uma semente Ed25519 de 32 bytes como 64 dígitos hexadecimais; forneça a sua (por exemplo, uma mantida em seu sistema de gestão de chaves) ou deixe que keygen gere uma. kshana avisa se o arquivo pode ser lido por outros usuários. .gitignore exclui *.evidence-key; nomeie sua chave dessa forma se ela estiver em um checkout.
O que há em um pacote#
| Arquivo | Conteúdo |
|---|---|
log-slice.bin |
os bytes brutos do log do receptor para a janela |
config.json |
o cenário como executado (cada limiar de monitor, evento e regra de comparação), os monitores que rodaram, a linha de base de calibração e o hash do cenário; para uma embarcação, também a declaração consultiva que toda saída de embarcação traz (software de caráter consultivo, não um equipamento de navegação homologado); a partir da 0,36, o geometry_model (a tabela [geometry] efetivamente usada, o peso do monitor de geometria, cada proveniência e as regras de classe). Nomes de diretórios e cargas embutidas são removidos. |
epochs.json |
cada época na janela com as estatísticas do motor, os alarmes (os motivos) e o estado de confiança; a partir da 0,36, também sua geometry de satélites (boa, degradada com motivos, ou desconhecida quando o log não traz geometria de satélites) e uma class em cada dedução (geometry, environment, inconsistency) |
summary.html |
um resumo autocontido e sem scripts: identificação, hashes, resultados, motivos, uma faixa de geometria (épocas por estado de geometria) separada das classes das deduções (0,36), cada limiar, a tabela por época e os limites do registro |
manifest.json |
versão do motor, janela, horário de criação, o formato e o nome do log, o SHA-256 do log completo, o intervalo e o SHA-256 da fatia, a chave pública e a impressão digital do signatário, o tamanho e o SHA-256 de cada arquivo e uma cadeia de hashes sobre os arquivos |
manifest.sig |
assinatura Ed25519 sobre os bytes exatos de manifest.json |
timestamp.tsr |
token de carimbo de tempo RFC 3161 opcional |
Fatia do log. O leitor informa o intervalo de bytes de origem de cada época, de modo que a fatia é o menor intervalo de bytes que contém todas as épocas da janela (byte-range): NMEA, u-blox UBX, observações RINEX 3 e logs do Android. Ela recebe hash como bytes brutos, nunca como texto decodificado. Para um feed ao vivo ou RINEX 2 não há intervalo; o log inteiro é então incluído e o manifesto diz whole-log (a janela limita quais épocas são informadas, não quais bytes são incluídos). O resumo declara qual caso se aplica. Uma fatia é um registro dos bytes da janela, não um log autônomo: por exemplo, uma fatia RINEX não tem cabeçalho de arquivo.
Cadeia de hashes. O manifesto lista os arquivos em uma ordem fixa. link₀ = SHA-256("kshana- evidence-chain/1"); para cada arquivo, linkᵢ = SHA-256(linkᵢ₋₁ ‖ SHA-256(file) ‖ len(name) ‖ name) (comprimento como 4 bytes, big-endian). O último elo é chain_head. Reordenar, remover ou substituir um arquivo altera a cadeia; a assinatura cobre a cadeia.
Verificar um pacote#
kshana evidence verify pack/ --pubkey signer.key.pub [--log session.nmea] \
[--require-timestamp] [--allow-unpinned] [--json]
O código de saída 0 significa verificado; 1 significa que algo falhou; 2 significa erro de uso; 3 significa que o pacote está íntegro, mas seu signatário não estava fixado (veja abaixo). Cada verificação informa êxito, falha ou omissão, e cada falha é nomeada:
| Código de falha | Significado |
|---|---|
manifest-missing, manifest-malformed, unsupported-format |
o manifesto não pode ser lido |
signature-missing, signature-malformed, signature-invalid |
sem assinatura, não é uma assinatura hexadecimal minúscula de 64 bytes, ou ela não é verificada sobre manifest.json |
public-key-malformed, public-key-mismatch |
a chave não é válida ou difere da que você forneceu |
file-missing, file-size-mismatch, file-hash-mismatch |
um arquivo listado está ausente ou difere do manifesto; o arquivo é nomeado |
unlisted-file |
um arquivo está no pacote, mas não no manifesto |
chain-mismatch, chain-head-mismatch |
um elo não é o que suas entradas resultam; o arquivo é nomeado |
artifact-list-malformed, slice-record-inconsistent, epoch-count-mismatch |
o manifesto contradiz a si mesmo ou epochs.json |
full-log-hash-mismatch, slice-not-from-full-log |
com --log: o log que você tem não é o registrado, ou a fatia não é esse intervalo dele |
timestamp-missing, timestamp-malformed, timestamp-imprint-mismatch |
veja abaixo |
Alterar um byte de qualquer arquivo faz a verificação falhar com o motivo correspondente: um artefato alterado é informado como divergência de hash daquele arquivo e nada mais; um manifesto alterado faz a assinatura falhar. Recalcular os hashes e a cadeia após editar um arquivo não ajuda sem a chave de assinatura; então falha apenas a assinatura.
Fixe a chave. Sem --pubkey, um pacote prova apenas que está íntegro em relação à chave que ele mesmo nomeia, a qual qualquer pessoa pode gerar. Por isso a verificação termina com 3 e diz "INTACT, BUT THE SIGNER IS NOT PINNED" em vez de "VERIFIED"; --allow-unpinned aceita conscientemente a chave do próprio pacote e imprime "VERIFIED against the key the pack names itself (signer NOT pinned; pass --pubkey)". Obtenha a chave pública do signatário junto ao próprio signatário por um meio em que você confie (não a partir do pacote) e passe-a com --pubkey: 64 dígitos hexadecimais ou o arquivo .pub. Passar a chave privada por engano é reconhecido e recusado. A impressão digital (os primeiros 128 bits do SHA-256 da chave, 32 dígitos hexadecimais) é impressa para comparação visual; fixar a chave completa é o caminho confiável.
Formato do pacote e procedimento de verificação (normativo)#
Esta seção é a especificação a partir da qual se escreve um verificador independente. Palavras em maiúsculas são requisitos. Um pacote é um diretório (ou qualquer mapa de nome de arquivo para bytes) de arquivos com os nomes exatamente como abaixo; um verificador trabalha com os bytes de cada arquivo como armazenados e nunca reserializa nada.
Arquivos. Os arquivos de artefato, nesta ordem fixa, são log-slice.bin, config.json, epochs.json, summary.html. Um pacote também tem manifest.json e manifest.sig, e PODE ter timestamp.tsr. Qualquer outro nome é um arquivo não listado.
Hexadecimal. Toda cadeia hexadecimal usa apenas 0-9a-f minúsculos, sem espaços; maiúsculas não são aceitas. Os valores SHA-256 têm 64 dígitos, a chave pública tem 64 dígitos, a assinatura tem 128 dígitos.
manifest.sig. O hexadecimal minúsculo de 128 dígitos da assinatura Ed25519 de 64 bytes (RFC 8032, o esquema Ed25519 puro) sobre os bytes exatos de manifest.json, seguido de exatamente um \n e nada mais. A assinatura DEVE ser verificada de forma estrita: uma assinatura cujo valor S não seja menor que a ordem do grupo L é inválida.
manifest.json. Um objeto JSON em UTF-8. Seus membros (um verificador DEVE encontrar cada um com o tipo JSON indicado; membros adicionais são ignorados):
| membro | tipo | significado |
|---|---|---|
format |
string | DEVE ser igual a kshana-evidence/1 |
title, engine_version, disclaimer |
string | descritivo |
created_utc |
string ou nulo | horário de criação, descritivo |
window |
objeto {from_s, to_s} |
números, segundos |
epochs_in_window |
inteiro não negativo | o número de entradas do array JSON em epochs.json |
log |
objeto | format string, file_name string, full_sha256 hexadecimal, full_bytes inteiro não negativo, start_label string ou nulo, e slice |
log.slice |
objeto | kind (exatamente byte-range ou whole-log; qualquer outro valor torna o manifesto malformado), start e end inteiros não negativos (deslocamentos em bytes, fim exclusivo), sha256 hexadecimal |
signer |
objeto | algorithm string (ed25519), public_key hexadecimal (32 bytes), fingerprint string |
artifacts |
array de objetos | um por arquivo de artefato, na ordem fixa: name string, role string, bytes inteiro não negativo, sha256 hexadecimal, link hexadecimal |
chain_head |
hexadecimal | o último elo |
lab_attestation |
objeto, opcional | presente apenas em um pacote gerado a partir de uma sessão de laboratório (--lab-attestation): a declaração do operador operator, organisation, date, setup (conducted ou shielded), attenuation_db ou enclosure, emitter, statement e, opcionalmente, signature, registrada como foi dada. Ela é coberta pela assinatura do manifesto e nunca é verificada; um verificador a informa como "attested by the named operator; not verified by Kshana". Ausente nos demais casos, e um pacote sem ela permanece inalterado |
signer.fingerprint são os primeiros 32 dígitos hexadecimais do SHA-256 sobre os 32 bytes da chave pública.
Cadeia de hashes. link0 é o SHA-256 dos 23 bytes ASCII kshana-evidence-chain/1. Para cada artefato, em ordem, link = SHA-256( previous_link (32 bytes) ‖ SHA-256(file) (32 bytes) ‖ len(name) as 4 bytes big-endian ‖ name as UTF-8 ), onde SHA-256(file) é tomado do sha256 registrado no manifesto para aquele artefato (assim a cadeia é uma propriedade do manifesto e é verificada em relação a ele, não aos arquivos).
Procedimento. Um verificador executa estas etapas em ordem e coleta todas as falhas; o resultado é o conjunto de falhas, cada uma com um code e, quando indicado, o name do arquivo. Ele NÃO DEVE parar na primeira falha, exceto onde indicado. pin é a chave pública em que o verificador foi instruído a confiar, se houver.
- Sem
manifest.json: falhamanifest-missing; pare. - Se
manifest.jsonnão é um objeto JSON com todos os membros acima nos tipos indicados (incluindo as entradas deartifactscom todos os membros, elog.slice): falhamanifest-malformed. Caso contrário, seformatnão é o valor acima:unsupported-format. - Fase da assinatura. Esta fase é executada quer o manifesto tenha sido legível ou não. Se o manifesto era legível, a chave nomeada é
signer.public_key; se não forem 64 dígitos hexadecimais minúsculos, falhapublic-key-malformed(sem chave nomeada); caso contrário, sesigner.fingerprintnão é a impressão digital dela, falhapublic-key-malformed(a chave continua utilizável). Se existem tanto umapinquanto uma chave nomeada e elas diferem: falhapublic-key-mismatch. A chave de verificação épin, se uma foi fornecida; do contrário, a chave nomeada, se houver; com nenhuma das duas, não há chave de verificação (isso inclui todos os casos em que o manifesto não era legível e nenhumapinfoi fornecida). Se não hámanifest.sig:signature-missing. Senão, se o arquivo não tem exatamente 128 dígitos hexadecimais minúsculos mais um\n:signature-malformed. Senão, se há uma chave de verificação que não é uma codificação válida de chave pública Ed25519:public-key-malformed. Senão, se há uma chave de verificação e a assinatura não é verificada de forma estrita sobre os bytes demanifest.json(sejam quais forem):signature-invalid. (Sem chave de verificação, a assinatura não é verificada e nenhuma falha é levantada por isso.) - Se o manifesto não era legível (a etapa 2 deu
manifest-malformed): pare aqui. - Artefatos. Se os nomes em
artifactsnão são exatamente os quatro acima, em ordem:artifact-list-malformed. Depois, para cada entrada do manifesto, em ordem: se o pacote não tem arquivo com esse nome,file-missing(nome); senão, se seu comprimento difere debytes,file-size-mismatch(nome); e, independentemente, se o SHA-256 do arquivo difere desha256,file-hash-mismatch(nome). - Arquivos não listados: para cada arquivo do pacote que não seja
manifest.json,manifest.sig,timestamp.tsrnem os nomes emartifactsdo manifesto:unlisted-file(nome). - Cadeia. Percorra
artifactsdo manifesto em ordem, a partir delink0. Se osha256de uma entrada não tem 64 dígitos hexadecimais minúsculos:manifest-malformede a cadeia é abandonada (sem verificação do cabeçalho). Caso contrário, calcule o elo; se ele difere dolinkda entrada:chain-mismatch(nome). Após o percurso, se a cadeia não foi abandonada e o último elo difere dechain_head:chain-head-mismatch. - Fatia e épocas. Levante
slice-record-inconsistent(no máximo uma vez por motivo, e um verificador informa o código uma única vez, não importa quantos motivos se apliquem) se ocorrer qualquer um de:start > end;end > full_bytes; osha256da entradalog-slice.bindifere deslice.sha256; obytesdessa entrada difere deend - start; oukindéwhole-loge qualquer um destart != 0,end != full_bytes,slice.sha256 != full_sha256. Seepochs.jsonexiste e é interpretado como um array JSON cujo comprimento difere deepochs_in_window:epoch-count-mismatch. - Log completo. Somente se o verificador recebeu os bytes do log original: se o SHA-256 deles difere de
log.full_sha256,full-log-hash-mismatch; caso contrário, se o intervalostart..endnão está dentro do log ou os bytes do log ali diferem delog-slice.bin,slice-not-from-full-log. - Carimbo de tempo. Sem
timestamp.tsr: se o verificador foi instruído a exigir um,timestamp-missing; caso contrário, nada. Com um: se ele não pode ser lido como um token ou resposta RFC 3161 (veja abaixo),timestamp-malformed; caso contrário, se a impressão da mensagem não é o hash, no algoritmo da própria impressão (SHA-256, SHA-384 ou SHA-512), dos bytes demanifest.json:timestamp-imprint-mismatch.
O pacote é verificado exatamente quando o conjunto de falhas está vazio. Se o signatário estava fixado é informado separadamente e não altera o conjunto de falhas.
Token de carimbo de tempo. Um token é um ContentInfo DER cujo tipo de conteúdo é CMS SignedData (1.2.840.113549.1.7.2) e cujo tipo de conteúdo encapsulado é id-ct-TSTInfo (1.2.840.113549.1.9.16.1.4); uma resposta é um TimeStampResp DER (um PKIStatusInfo seguido do token) cujo status é 0 ou 1. O algoritmo da impressão da mensagem de TSTInfo deve ser SHA-256, SHA-384 ou SHA-512, com um digest do comprimento desse algoritmo, e seu genTime um horário de calendário UTC real. BER de comprimento indefinido não é aceito. A assinatura e a cadeia de certificados da autoridade só são verificadas quando âncoras de confiança são fornecidas (--tsa-ca, abaixo).
O rótulo de evidência (0,40)#
Um pacote criado pela 0,40 ou posterior traz, dentro de config.json, uma entrada evidence (o nível, VALIDATED, MODELLED ou PARTNER, que é o estado mais fraco entre suas linhas, e os ids e requisitos das linhas da matriz de verificação em que o conteúdo do pacote se apoia: a linha de integridade do pacote, as linhas de receiver-trust e as linhas de pontuação do jammertest ou de ida e volta para esses pacotes) e config_version "1.1". Está dentro de config.json, de modo que o hash do arquivo do manifesto, a cadeia de hashes e a assinatura o cobrem: alterá-lo faz a verificação falhar. kshana evidence verify informa o nível e os ids das linhas.
A mudança é aditiva e apenas para frente. O format do manifesto permanece kshana-evidence/1: o texto normativo acima diz que um verificador DEVE encontrar esse valor, e o verificador de sala limpa em scripts/ o encontra. A versão menor é portanto carregada por config_version (ausente em um pacote feito antes da 0,40, significando 1,0), e não por uma nova cadeia format. Um pacote feito antes da 0,40 não tem rótulo e é verificado sem alteração; kshana evidence verify diz "no evidence label (pre-0.40 pack)". Os testes são tests/evidence_pack_label.rs (incluindo os pacotes de referência independente pré-0.40 versionados) e tests/evidence_labels_doc_sync.rs.
Carimbo de tempo RFC 3161 (opcional)#
Um token de carimbo de tempo de uma autoridade de carimbo de tempo mostra que o manifesto existia em um instante que a autoridade atesta. O token é sobre manifest.json, por isso é solicitado depois que o pacote é gerado (não há como fornecer um na criação do pacote, e create_bundle recusa um token que não seja sobre exatamente esse manifesto):
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 lê o token (um token simples ou uma resposta completa), verifica que ele foi concedido e que a impressão da mensagem é igual ao hash de manifest.json, e imprime o horário que ele declara. Sem âncoras de confiança, acrescenta "timestamp authority signature not verified by Kshana" e informa authority_signature_verified: false; execute a linha openssl ts -verify acima para essa parte. Um token cuja impressão difere, ou que não é interpretado, é uma falha. Os tipos de conteúdo, o comprimento da impressão e o horário de geração do token são verificados quanto à coerência.
Com --tsa-ca authority-ca.pem (um ou mais certificados PEM, todos confiáveis), verify também verifica a autoridade, offline, como faz openssl ts -verify: o digest da mensagem dos atributos assinados, a assinatura (RSA PKCS#1 v1.5, RSA-PSS, ECDSA P-256 ou P-384), a vinculação ESS do certificado de assinatura, o uso estendido de chave timeStamping crítico do signatário e a cadeia até uma âncora autoemitida, com todos os certificados válidos no genTime do token. Uma falha é informada como timestamp-authority-invalid com a primeira verificação que falhou, e authority_signature_verified é verdadeiro somente quando tudo passou. A revogação (CRL ou OCSP) não é verificada. Este verificador concorda com o OpenSSL 3.0.13 em 27 casos autogerados (tests/fixtures/evidence_tsa/PREREGISTRATION.md).
O token fica ao lado do manifesto assinado, não dentro dele. Nada na assinatura ou na cadeia o cobre, de modo que um token pode ser removido, ou um outro token válido substituído, sem que a assinatura falhe. Por isso verify observa quando um pacote não tem nenhum, e --require-timestamp torna a ausência de token uma falha (timestamp-missing); um token substituído só é detectado se não corresponder ao manifesto. attach-timestamp recusa sobrescrever um token existente, a menos que você passe --replace. Se um carimbo de tempo importa para você, guarde o token e o resultado da verificação junto a seus próprios registros, e exija-o.
A partir do código#
kshana::evidence::build_receiver_trust_pack é o único ponto de entrada que a linha de comando e todas as outras superfícies chamam para gerar um pacote: entram um cenário, os bytes do log (e os do arquivo de navegação, para RINEX), a janela como texto, um título e um horário de criação opcional, além da semente de assinatura; saem os arquivos, o número de épocas, a fatia, o hash do manifesto e a impressão digital do signatário. Ele não lê arquivos nem relógio, de modo que as mesmas entradas dão os mesmos bytes. Abaixo dele, create_bundle e verify_bundle são funções puras: bytes entram (EvidenceInput, uma semente de 32 bytes, um BTreeMap<String, Vec<u8>> de arquivos), resultados serializáveis saem (Files, VerifyReport). Não usam arquivos, relógio nem estado de processo, e compilam para wasm32, de modo que as superfícies Python, WebAssembly e de servidor as chamam sem alteração. A linha de comando é um invólucro fino sobre elas.
O que um pacote não faz#
- Ele não reexecuta a avaliação. Qualquer pessoa com o log completo e
config.jsonpode reexecutarkshana receiver-truste comparar comepochs.json; o pacote prova o que foi registrado, não que o motor estava certo. - Ele não prova que o log é genuíno ou completo. O hash do log completo vincula o registro a um único arquivo; a origem desse arquivo cabe ao operador estabelecer.
- Ele não traz nenhuma afirmação de desempenho sobre detecção ou rejeição de interferência.
- É um registro técnico e não um parecer jurídico nem uma certificação.