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:
- ARRL’s LoTW developer documentation: developer information,
the signed log,
.tq8, submitting QSOs, LoTW’s PKI, certificate security, station locations, TQSL’s command line, and the LoTW Trusted Partners guidelines. - TrustedQSL: its source at v2.8.7, read for what LoTW
receives; tqsllib’s API documentation; and
TQSL’s change log. QG Trust carries two things of
TQSL’s, both ARRL’s, under ARRL’s terms: the TQSL configuration (
config.xml), from which the LoTW tables are generated, and ARRL’s root and CA certificates (the 2015 pair as TQSL embeds them, the 2023 pair as ARRL issues them in a.tq6). TQSL 2.8.7, built from its own source, is the reference QG Trust’s signed records and signatures are tested against, byte for byte. - Other open-source projects: qso-web, above, and a survey of 126 ham-radio Rust crates on crates.io, which found no other LoTW signing crate.
- Standards: ADIF 3.1.7; RFC 5280 (X.509 certificates), RFC 4055 and RFC 8017 (RSA), RFC 1952 (gzip); W3C XML 1.0.
§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_pkcs12orStore::inspect_tbk, review the public preview, then consume it withStore::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>VALUEor<NAME:LENGTH:TYPE>VALUE, the value exactly LENGTH bytes, and records ended by<eor>. Key files (keys/<call>) and signed logs (.tq7, and.tq8once decompressed) are written this way, as ADIF is. - request
- Callsign certificate requests compatible with TQSL 2.8.7 (
openssl_cert.cpp,tqsl_createCertRequestandtqsl_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.tq8is the same, gzipped. - signing
- Signing a QSO record as LoTW accepts it: RSA PKCS#1 v1.5 over SHA-1, the
SIGN_LOTW_V2.0field, as tqsllib signs (TQSL 2.8.7src/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(orTEST_SERVICEin 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.