Skip to main content

Crate qso_graph_trust

Crate qso_graph_trust 

Source
Expand description

§qso-graph-trust

LoTW signing for your own programs: sign ADIF (.adi or .adx) and Cabrillo logs with your LoTW certificate, so ARRL’s Logbook of The World (LoTW) accepts them.

  • Memory-safe Rust, with signatures made by AWS-LC, Amazon’s open-source cryptography library.
  • Every QSO checked by ARRL’s own rules (bands, modes, DXCC entities, dates, satellites) before it is signed; one LoTW would refuse is named, with the field and the reason.
  • The key can only sign: no function returns it, and it is never printed or logged.

It is the library behind the qg-trust command line and QSO Graph Desktop’s LoTW signing.

§Example

Sign an ADIF log with the certificate in a .tbk file, and write the .tq8 LoTW accepts. The example also uses zeroize and zeroizing-alloc (cargo add zeroize zeroizing-alloc@=0.1.1):

use std::time::SystemTime;

use qso_graph_trust::certificate::Trust;
use qso_graph_trust::qso::Qso;
use qso_graph_trust::signed_log::{sign_log, tq8};
use qso_graph_trust::{records, tbk};
use zeroize::Zeroizing;

// Every program that signs installs a wiping allocator, so no key text is left in memory it frees,
// whichever crate allocated it. QSO Graph Trust's own programs use exactly this one.
#[global_allocator]
static ALLOCATOR: zeroizing_alloc::ZeroAlloc<std::alloc::System> =
    zeroizing_alloc::ZeroAlloc(std::alloc::System);

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // A certificate is trusted only if it chains to LoTW's own root, pinned here.
    let tbk = tbk::read(&std::fs::read("tqslconfig.tbk")?, &Trust::lotw())?;

    let entry = tbk.certificates.iter().find(|c| c.callsign == "K1ABC").ok_or("no certificate")?;
    let certificate = entry.certificate.as_ref().ok_or("no certificate yet")?;
    // Ask for the key's password when the program runs: never on a command line, in a file or in code.
    let password: Zeroizing<Vec<u8>> = ask_for_password()?;
    let key = entry.key.as_ref().ok_or("no key")?.unlock(Some(&password))?;
    let station = &tbk.locations.iter().find(|l| l.name == "Home").ok_or("no location")?.station;

    // Every QSO is checked as LoTW checks it; one it would refuse is an error naming the field.
    let qsos = records::parse(&std::fs::read("log.adi")?)?
        .iter()
        .map(Qso::from_adif)
        .collect::<Result<Vec<_>, _>>()?;

    let signed = sign_log(certificate, &key, station, &qsos, SystemTime::now())?;
    std::fs::write("log.tq8", tq8(&signed))?;
    Ok(())
}

ask_for_password is your program’s: a prompt that does not echo, or the OS’s own dialog.

The .tq8 is what LoTW accepts: upload it on LoTW’s website.

§Checking a download

Each release is published by this repository’s release workflow, which GitHub attests: a signed statement that the .crate file was built by that workflow from that commit. With the GitHub CLI:

gh attestation verify qso-graph-trust-<version>.crate -R qso-graph/qso-graph-trust

§Credits

QSO Graph Trust stands on other people’s work.

TrustedQSL (TQSL) and the people who built it and keep it running. TQSL’s contributors, as its AUTHORS.txt lists them, are WA1GON, K1MK, KE3Z, K1MU, KC2YWE and AA6YQ, with ARRL. Since 2003 their work has carried the signature on every QSO confirmed through Logbook of The World: a signing format, a certificate system and a program that hams on every platform rely on, kept current through two decades of operating systems, library changes and new modes, as volunteers. Thank you.

ARRL, for LoTW itself and for documenting it for developers.

rfrht’s qso-web, a shell script that builds and signs a .tq8 with OpenSSL, without TQSL, and uploads it to LoTW, showing that LoTW accepts a log signed by another program.

§Sources

QG Trust is a ground-up rewrite in Rust, not a method-by-method port of TQSL. What it does was decided from these, and how is QG Trust’s own:

§Licence

LoTW’s tables are generated from ARRL’s TQSL configuration, Copyright (C) 2001-2015 American Radio Relay League, Inc., portions (C) 2003-2026 The TrustedQSL Developers, redistributed under ARRL’s terms in LICENSE-ARRL.md (ARRL’s text, unchanged), which ships with this crate.

GPL-3.0-or-later. Credits, notices and the source: https://github.com/qso-graph/qso-graph-trust.

Modules§

certificate
A LoTW callsign certificate: what a signed log’s tCERT record carries, and what decides which QSOs it may sign. LoTW’s own extensions name the DXCC entity and the first and last QSO date the certificate covers; the callsign is an attribute of the subject.
export
Password-protected export parts. Only a freshly generated password can seal a part; typed passwords can open it. Save the generated password separately: it is never stored in the part.
files
Bounded reads of regular files, shared by the library and command line.
import
Bounded certificate import. Inspect with Store::inspect_tq6, Store::inspect_pkcs12 or Store::inspect_tbk, review the public preview, then consume it with Store::commit. Decrypted keys stay inside the pending value and are wiped on drop. The one-step convenience methods use this same path. The whole file is validated before any store mutation. Errors contain categories and public certificate details only.
options
Caller-selected settings that affect signing. These are local to one conversion; importing a backup never changes process-wide settings. Construct QSOs with the same options used to sign.
qso
A QSO as LoTW receives it, from an ADIF record: the call in capitals, LoTW’s band and mode, the frequency only if it is in that band, and the date and time in LoTW’s form. A QSO LoTW would refuse is refused here, saying which field and why.
records
tqsllib’s record format: fields written as <NAME:LENGTH>VALUE or <NAME:LENGTH:TYPE>VALUE, the value exactly LENGTH bytes, and records ended by <eor>. Key files (keys/<call>) and signed logs (.tq7, and .tq8 once decompressed) are written this way, as ADIF is.
request
Callsign certificate requests compatible with TQSL 2.8.7 (openssl_cert.cpp, tqsl_createCertRequest and tqsl_sign_base64_data). Nothing is sent to ARRL here.
signed_log
A signed log, as LoTW receives it: an identification field, then a tCERT record with the certificate, a tSTATION record with the station location, and a tCONTACT record for each QSO, each carrying what was signed and its signature. This is the .tq7; a .tq8 is the same, gzipped.
signing
Signing a QSO record as LoTW accepts it: RSA PKCS#1 v1.5 over SHA-1, the SIGN_LOTW_V2.0 field, as tqsllib signs (TQSL 2.8.7 src/openssl_cert.cpp:2215, EVP_SignInit(ctx, EVP_sha1())).
station
A station location: where the QSOs were made from, as LoTW receives it in a signed log’s tSTATION record, and the station’s part of every signature. Every field is checked against the configuration’s fields for the location’s DXCC entity, and a list field’s value must be one of its list’s.
store
The shared QG Trust folder. Public records live in a bounded, versioned XML file; private PKCS#8 DER stays in a library-owned backend scoped to SERVICE (or TEST_SERVICE in tests). Each operation reloads the file while holding its OS lock, so separate library releases and processes cannot lose each other’s writes. Unknown versions are refused before any mutation. The file also keeps the LoTW roots the operator has accepted beside the pinned ones (Store::trust, Store::trusted_roots, Store::remove_root).
tbk
TQSL’s backup, the .tbk: every certificate, key and station location a ham’s TQSL holds, and the QSOs it has signed, as TQSL’s app writes them (apps/tqsl.cpp, MyFrame::BackupConfig): gzipped XML, <TQSL_Configuration> holding <Certificates>, <Locations>, <TQSLSettings> and <DupeDb>.
tqsl_locations
Read TQSL’s station locations on request, without changing its files.
upload
Prepare a signed log for LoTW and interpret its answer. Nothing here opens a connection, reads configuration, or performs file I/O. The caller supplies its own HTTP/TLS transport (Desktop uses its credential core), always verifies the server certificate and hostname, sets a finite timeout, refuses redirects to another host or to HTTP, and never takes proxy settings from a file. TLS verification must remain enabled on every attempt.

Constants§

VERSION
This library’s version, as Cargo has it.