Tradução automática, não revista por um falante nativo. O inglês é a referência.

Validação e evidênciasEditar no GitHubFonte: docs/EVIDENCE-PACKS.md

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#

A estrutura de um pacote de evidências. Um log de receptor (NMEA, u-blox UBX, RINEX 3 ou um log do Android) e uma janela de tempo entram em kshana receiver-trust evidence, que grava sete arquivos: log-slice.bin, config.json, epochs.json, summary.html, manifest.json, manifest.sig e um timestamp.tsr opcional. O manifesto registra o SHA-256 do log inteiro e de cada arquivo, além de uma cadeia de hashes sobre os arquivos; a assinatura cobre o manifesto. kshana evidence verify verifica a assinatura, o hash de cada arquivo, a cadeia e que nenhum arquivo não listado esteja presente, e termina com 0 quando verificado, 1 quando algo falhou e 3 quando o signatário não estava fixado. Um pacote é um registro técnico, não um parecer jurídico.A estrutura de um pacote de evidências. Um log de receptor (NMEA, u-blox UBX, RINEX 3 ou um log do Android) e uma janela de tempo entram em kshana receiver-trust evidence, que grava sete arquivos: log-slice.bin, config.json, epochs.json, summary.html, manifest.json, manifest.sig e um timestamp.tsr opcional. O manifesto registra o SHA-256 do log inteiro e de cada arquivo, além de uma cadeia de hashes sobre os arquivos; a assinatura cobre o manifesto. kshana evidence verify verifica a assinatura, o hash de cada arquivo, a cadeia e que nenhum arquivo não listado esteja presente, e termina com 0 quando verificado, 1 quando algo falhou e 3 quando o signatário não estava fixado. Um pacote é um registro técnico, não um parecer jurídico.
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.

  1. Sem manifest.json: falha manifest-missing; pare.
  2. Se manifest.json não é um objeto JSON com todos os membros acima nos tipos indicados (incluindo as entradas de artifacts com todos os membros, e log.slice): falha manifest-malformed. Caso contrário, se format não é o valor acima: unsupported-format.
  3. 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, falha public-key-malformed (sem chave nomeada); caso contrário, se signer.fingerprint não é a impressão digital dela, falha public-key-malformed (a chave continua utilizável). Se existem tanto uma pin quanto uma chave nomeada e elas diferem: falha public-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 nenhuma pin foi 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 de manifest.json (sejam quais forem): signature-invalid. (Sem chave de verificação, a assinatura não é verificada e nenhuma falha é levantada por isso.)
  4. Se o manifesto não era legível (a etapa 2 deu manifest-malformed): pare aqui.
  5. Artefatos. Se os nomes em artifacts nã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 de bytes, file-size-mismatch (nome); e, independentemente, se o SHA-256 do arquivo difere de sha256, file-hash-mismatch (nome).
  6. Arquivos não listados: para cada arquivo do pacote que não seja manifest.json, manifest.sig, timestamp.tsr nem os nomes em artifacts do manifesto: unlisted-file (nome).
  7. Cadeia. Percorra artifacts do manifesto em ordem, a partir de link0. Se o sha256 de uma entrada não tem 64 dígitos hexadecimais minúsculos: manifest-malformed e a cadeia é abandonada (sem verificação do cabeçalho). Caso contrário, calcule o elo; se ele difere do link da entrada: chain-mismatch (nome). Após o percurso, se a cadeia não foi abandonada e o último elo difere de chain_head: chain-head-mismatch.
  8. 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; o sha256 da entrada log-slice.bin difere de slice.sha256; o bytes dessa entrada difere de end - start; ou kind é whole-log e qualquer um de start != 0, end != full_bytes, slice.sha256 != full_sha256. Se epochs.json existe e é interpretado como um array JSON cujo comprimento difere de epochs_in_window: epoch-count-mismatch.
  9. 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 intervalo start..end não está dentro do log ou os bytes do log ali diferem de log-slice.bin, slice-not-from-full-log.
  10. 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 de manifest.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.json pode reexecutar kshana receiver-trust e comparar com epochs.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.