Data Integrity¶
Scenario¶
You downloaded an APNIC stats file (delegated, extended, assigned, IPv6-assigned, or legacy) — possibly from a mirror, possibly a cached archive. Before trusting it, you must prove it is authentic and unmodified: the local MD5 must match APNIC's published .md5 sidecar, and the PGP signature (.asc) must verify against APNIC's published signing public key. This is the trust root for every downstream workflow.
Composition¶
| Layer | Method / Command | Purpose |
|---|---|---|
| End-to-end MD5 | VerifyMD5(ctx, dataType, date) / apnic verify integrity | Download the data file + .md5 sidecar, compute locally, compare. |
| Raw MD5 | FetchMD5Checksum(ctx, dataType, date) / apnic verify md5 | The published checksum string (BSD- and GNU-compatible). |
| PGP signature | FetchASCSignature(ctx, dataType, date) / apnic verify asc | The detached .asc signature for the data file. |
| Public key | FetchPublicKey(ctx) / apnic verify pubkey | APNIC's CURRENT_PUBLIC_KEY for verifying .asc. |
| Cumulative transfers | FetchTransfersAllMD5, FetchTransfersAllASC | Integrity for the transfers-all cumulative file. |
Supported dataType values: delegated, delegated-extended, assigned, delegated-ipv6-assigned, legacy. The .asc path does not exist for delegated-ipv6-assigned.
flowchart TB
subgraph APNIC["APNIC publishes"]
DATA["stats data file<br/>e.g. delegated-apnic-latest"]
MD5["data.md5<br/>sidecar checksum"]
ASC["data.asc<br/>PGP signature"]
PUB["CURRENT_PUBLIC_KEY<br/>signing key"]
end
subgraph VERIFY_MD5["MD5 path"]
DL_DATA["download data"]
DL_MD5["download .md5"]
CALC["compute local MD5"]
CMP["compare strings"]
MD5_OK["OK / FAIL"]
end
subgraph VERIFY_PGP["PGP path"]
DL_ASC["download .asc"]
DL_PUB["fetch public key"]
IMPORT["import key"]
SIGCHECK["verify signature<br/>against data"]
PGP_OK["valid / invalid"]
end
DATA --> DL_DATA --> CALC
MD5 --> DL_MD5 --> CMP
CALC --> CMP --> MD5_OK
ASC --> DL_ASC --> SIGCHECK
DATA --> SIGCHECK
PUB --> DL_PUB --> IMPORT --> SIGCHECK --> PGP_OK Flow: end-to-end verification¶
sequenceDiagram
participant Op as Operator
participant CLI as apnic CLI
participant SDK as SDK Client
participant FTP as ftp.apnic.net
Note over Op,FTP: MD5 integrity path
Op->>CLI: verify integrity --type delegated
CLI->>SDK: VerifyMD5(ctx, "delegated", "")
SDK->>FTP: GET delegated-apnic-latest
FTP-->>SDK: data bytes
SDK->>FTP: GET delegated-apnic-latest.md5
FTP-->>SDK: BSD-style "MD5 (file) = <hash>"
SDK->>SDK: compute local MD5, normalize, compare
SDK-->>CLI: nil on match, error on mismatch
CLI-->>Op: OK: delegated MD5 verified (exit 0)
Note over Op,FTP: PGP signature path
Op->>CLI: verify asc --type delegated
CLI->>SDK: FetchASCSignature(ctx, "delegated", "")
SDK->>FTP: GET delegated-apnic-latest.asc
FTP-->>Op: detached signature text
Op->>CLI: verify pubkey
CLI->>SDK: FetchPublicKey(ctx)
SDK->>FTP: GET CURRENT_PUBLIC_KEY
FTP-->>Op: ASCII-armored public key
Note over Op: import key + gpg --verify data.asc data Go example¶
package main
import (
"context"
"fmt"
"log"
apnic "github.com/cyberspacesec/apnic-skills"
)
// VerifyAll runs end-to-end MD5 verification across every stats file type.
func VerifyAll(ctx context.Context, client *apnic.Client) error {
types := []string{
"delegated",
"delegated-extended",
"assigned",
"delegated-ipv6-assigned",
"legacy",
}
for _, t := range types {
if err := client.VerifyMD5(ctx, t, ""); err != nil {
return fmt.Errorf("integrity FAIL for %s: %w", t, err)
}
fmt.Printf("OK: %s MD5 verified\n", t)
}
return nil
}
// FetchPGPMaterial fetches the signature and public key for offline verification.
func FetchPGPMaterial(ctx context.Context, client *apnic.Client, dataType string) error {
// 1. The detached PGP signature for the data file.
asc, err := client.FetchASCSignature(ctx, dataType, "")
if err != nil {
return fmt.Errorf("fetch asc: %w", err)
}
fmt.Printf("signature (%s): %d bytes\n", dataType, len(asc))
// 2. APNIC's current signing public key.
pub, err := client.FetchPublicKey(ctx)
if err != nil {
return fmt.Errorf("fetch pubkey: %w", err)
}
fmt.Printf("public key: %d bytes\n", len(pub))
// 3. The published MD5 (raw string, for external tooling).
md5, err := client.FetchMD5Checksum(ctx, dataType, "")
if err != nil {
return fmt.Errorf("fetch md5: %w", err)
}
fmt.Printf("md5: %s\n", md5)
// Offline verification (external gpg):
// echo "$pub" | gpg --import
// gpg --verify <(echo "$asc") <data file>
return nil
}
func main() {
client := apnic.NewClient()
ctx := context.Background()
if err := VerifyAll(ctx, client); err != nil {
log.Fatalf("integrity check failed: %v", err)
}
if err := FetchPGPMaterial(ctx, client, "delegated"); err != nil {
log.Fatal(err)
}
}
CLI combination¶
# 1) End-to-end MD5 integrity (downloads data + sidecar, compares locally)
apnic verify integrity --type delegated
apnic verify integrity --type delegated-extended
apnic verify integrity --type assigned
apnic verify integrity --type delegated-ipv6-assigned
apnic verify integrity --type legacy
# 2) Raw published MD5 checksum (BSD-style "MD5 (file) = <hash>")
apnic verify md5 --type delegated
# 3) Detached PGP signature
apnic verify asc --type delegated
# 4) APNIC's current signing public key (for offline gpg --verify)
apnic verify pubkey
Variant: verify a historical snapshot¶
Variant: batch-verify every type, fail fast¶
for t in delegated delegated-extended assigned delegated-ipv6-assigned legacy; do
if apnic verify integrity --type "$t"; then
echo "OK $t"
else
echo "FAIL $t"
fi
done
One-shot script: full integrity sweep with alarm¶
#!/usr/bin/env bash
# integrity-sweep.sh — verify every stats file type; non-zero exit if any fails.
set -uo pipefail # no -e: we want to check every type, then report
types=(delegated delegated-extended assigned delegated-ipv6-assigned legacy)
fail=0
for t in "${types[@]}"; do
if apnic verify integrity --type "$t" >/dev/null 2>&1; then
echo "OK $t"
else
echo "FAIL $t"
fail=1
fi
done
exit "$fail"
Offline PGP verification¶
Once you have the data file, the .asc, and the public key, verify the signature with GnuPG outside the SDK:
# Import APNIC's signing key
apnic verify pubkey | gpg --import
# Verify the detached signature against the data file
apnic verify asc --type delegated > /tmp/delegated.asc
# (download or reuse the data file, e.g. via the SDK cache or apnic delegated)
gpg --verify /tmp/delegated.asc <path-to-delegated-apnic-latest>
A Good signature line from gpg --verify confirms the file was signed by APNIC's key and has not been altered.
Expected output¶
verify integritysuccess:OK: delegated (date=latest) MD5 verified, exit code0.verify integrityfailure: non-zero exit code with an error message (content mismatch, network error, or missing sidecar).verify md5: the BSD-style checksum stringMD5 (delegated-apnic-latest) = <hash>.verify asc: the ASCII-armored PGP signature block.verify pubkey: the ASCII-armoredCURRENT_PUBLIC_KEYblock.
Notes¶
VerifyMD5is the one-call trust check: it downloads both the data file and its.md5sidecar, computes the local MD5, normalizes the BSD/GNU formats difference, and compares. Use it instead of hand-rolling the comparison.- The
.ascPGP signature path does not exist fordelegated-ipv6-assigned; only the MD5 path applies to that file. date=""means "latest"; passYYYYMMDDto verify an archived snapshot.- The same integrity primitives apply to the cumulative
transfers-allfile viaFetchTransfersAllMD5/FetchTransfersAllASC— see the Transfer Tracking workflow. - In CI, gate downstream parsing on a successful
verify integrityso a tampered or truncated file never reaches your audit pipeline.