Adds option/arg to allow minimal devices to make TLS insecure requests if needed, also adds compile.go build driver

This commit is contained in:
markmental 2026-09-08 20:14:46 -04:00
commit ced950004a
7 changed files with 378 additions and 33 deletions

View file

@ -39,12 +39,51 @@ Run the test suite with:
go test ./...
```
### Cross-compiling with compile.go
`compile.go` (repo root) is the supported build driver. It cross-compiles
`./cmd/isthislegit` for a platform/arch matrix, always with
`CGO_ENABLED=0`, so every target builds without a cross C toolchain:
```bash
go run ./compile.go --platform=linux --arch=amd64
```
| Flag | Default | Description |
|--------------|---------|----------------------------------------------------------|
| `--platform` | `linux` | Target OS (see vocabulary below). |
| `--arch` | `amd64` | Target arch (see vocabulary below). |
| `--static` | `true` | `true` for static (`CGO_ENABLED=0`), `false` for dynamic (`CGO_ENABLED=1`, host builds only unless `CC` points at a cross compiler). Both modes are stripped via `-ldflags="-s -w"`. |
Platform vocabulary (case-insensitive): `linux`; `mac`/`macos`/`osx`/`darwin`
→ `darwin`; `windows`/`win` → `windows`; `bsd` → all three of `freebsd`,
`openbsd`, `netbsd` in one run (or name a single BSD explicitly).
Arch vocabulary: raw `GOARCH` names plus aliases — `x64`/`x86-64` → `amd64`;
`x86`/`i386`/`32bit` → `386`; `aarch64` → `arm64`; `arm`/`armv7`/`armv6`/`armv5`
→ `arm` with matching `GOARM`. Unknown values exit 1, and combos the
toolchain does not support (e.g. `darwin/386`) are refused with a clear
error.
Output binaries are named `isthislegit-<goos>-<goarch>` (with a `vGOARM`
suffix when applicable, e.g. `isthislegit-linux-armv7`, and `.exe` on
Windows):
```bash
go run ./compile.go --platform=linux --arch=amd64
go run ./compile.go --platform=windows --arch=x64 --static=false
go run ./compile.go --platform=bsd --arch=x64
go run ./compile.go --platform=linux --arch=armv7 # embedded ARM (Miyoo Mini Plus)
```
## Flags
| Flag | Description |
|-----------------|---------------------------------------------------------------|
| `-v, --verbose` | Append raw cert policy OIDs, TLS details, and RDAP JSON. |
| `-j, --json` | Output the result summary as JSON for pipeline scripting. |
| `-j, --json` | Output the result summary as JSON for scripting. |
| `-k, --insecure`| Skip TLS verification for RDAP |
| `--timeout=DUR` | Per-call deadline for DNS/TLS/RDAP (Go duration, default 5s). |
| `--scale` | Print the scoring scale reference table and exit. |
| `-h, --help` | Display usage and exit. |
@ -53,9 +92,27 @@ Examples:
```bash
isthislegit jdsoft.com jdsoftcareers.com
isthislegit -j example.com suspect-example.com
isthislegit --timeout=10s example.com suspect-example.com
isthislegit --scale
```
### Embedded systems (no CA certificates)
`isthislegit` runs on embedded ARM devices (verified on a Miyoo Mini Plus),
but minimal firmware images often ship without a CA certificate store. In
that case DNS resolution and certificate inspection still work, while
verified HTTPS (RDAP) fails — the fail-closed error will say so directly
(`x509: certificate signed by unknown authority`). Prefer installing
`ca-certificates` and fixing the system clock where possible; otherwise use
insecure mode:
```bash
isthislegit -k jdsoft.com jdsoftcareers.com
```
`--insecure` prints a warning and weakens results, so treat it as a last
resort for devices without stored CA certificates.
## Example output
```text
@ -104,8 +161,12 @@ VERDICT: LIKELY UNRELATED / MISCONFIGURED
Lookups fail closed: if RDAP dates or TLS data cannot be retrieved for
either domain, the tool errors out instead of printing a verdict built on
missing data. Fetches run concurrently under a 15s global budget with 5s
per-call deadlines (3 attempts each).
missing data, and the message names the underlying cause(s) (e.g. a TLS
verification failure, which on minimal devices usually means missing CA
certificates or a wrong system clock). Fetches run concurrently under a
15s global budget with 5s per-call deadlines (3 attempts each, adjustable
via `--timeout`); `--insecure` skips RDAP TLS verification as a last
resort, with a warning.
## Project layout

View file

@ -6,9 +6,11 @@ package main
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"os"
"strings"
"time"
"golang.org/x/sync/errgroup"
@ -22,7 +24,7 @@ import (
const version = "1.01"
const perCall = 5 * time.Second
const defaultPerCall = 5 * time.Second
const globalTimeout = 15 * time.Second
func usage() string {
@ -35,22 +37,28 @@ Usage:
Options:
-v, --verbose Output raw JSON responses from RDAP and full TLS cert details.
-j, --json Output summary results in raw JSON format for scripting.
-k, --insecure Skip TLS verification for RDAP (weakens results; use only
on devices without CA certificates). Prints a warning.
--timeout=DUR Per-call deadline for DNS/TLS/RDAP (Go duration, default 5s).
--scale Show the scoring scale table and exit (no domains required).
-h, --help Display this usage banner and exit.
Examples:
isthislegit jdsoft.com jdsoftcareers.com
isthislegit -j example.com suspect-example.com
isthislegit --timeout=10s example.com suspect-example.com
`, version)
}
type domainResult struct {
info score.DomainData
cert *cert.Details
rdap *rdap.Result
info score.DomainData
cert *cert.Details
rdap *rdap.Result
rdapErr error
certErr error
}
func fetchDomain(ctx context.Context, client *http.Client, domain string, now time.Time) domainResult {
func fetchDomain(ctx context.Context, client *http.Client, domain string, now time.Time, perCall time.Duration) domainResult {
res := domainResult{info: score.DomainData{Domain: domain}}
var g errgroup.Group
@ -59,21 +67,17 @@ func fetchDomain(ctx context.Context, client *http.Client, domain string, now ti
g.Go(func() error {
r, err := rdap.Query(ctx, client, domain, perCall)
if err != nil {
return err
}
rd = r
return nil
rd, res.rdapErr = r, err
return err
})
g.Go(func() error {
c, err := cert.InspectDomain(ctx, domain, perCall)
if err != nil {
return err
}
cd = c
return nil
cd, res.certErr = c, err
return err
})
_ = g.Wait() // Fail-open per field; fail-closed verdict comes later.
// Fail-open per field; the root causes are kept for the
// fail-closed verdict gate below.
_ = g.Wait()
res.rdap = rd
res.cert = cd
@ -109,7 +113,8 @@ func fetchDomain(ctx context.Context, client *http.Client, domain string, now ti
}
func run() int {
var verbose, jsonOut bool
var verbose, jsonOut, insecure bool
var perCall = defaultPerCall
var positionals []string
args := os.Args[1:]
seenDashDash := false
@ -119,11 +124,34 @@ func run() int {
positionals = append(positionals, a)
continue
}
if strings.HasPrefix(a, "--timeout=") {
d, err := time.ParseDuration(strings.TrimPrefix(a, "--timeout="))
if err != nil || d <= 0 {
fmt.Fprintf(os.Stderr, "[!] Error: Invalid --timeout value '%s'. Use a Go duration like 5s or 10s.\n", a)
return 1
}
perCall = d
continue
}
switch a {
case "-v", "--verbose":
verbose = true
case "-j", "--json":
jsonOut = true
case "-k", "--insecure":
insecure = true
case "--timeout":
if i+1 >= len(args) {
fmt.Fprintln(os.Stderr, "[!] Error: --timeout requires a value (e.g. --timeout=10s).")
return 1
}
i++
d, err := time.ParseDuration(args[i])
if err != nil || d <= 0 {
fmt.Fprintf(os.Stderr, "[!] Error: Invalid --timeout value '%s'. Use a Go duration like 5s or 10s.\n", args[i])
return 1
}
perCall = d
case "--scale":
fmt.Print(report.Scale(version))
return 0
@ -164,7 +192,7 @@ func run() int {
for _, d := range []string{legit, suspect} {
d := d
dg.Go(func() error {
if err := dns.CheckDNS(ctx, d); err != nil {
if err := dns.CheckDNS(ctx, d, perCall); err != nil {
return fmt.Errorf("domain '%s' failed DNS resolution (NXDOMAIN or offline). Aborting", d)
}
return nil
@ -176,22 +204,43 @@ func run() int {
}
client := &http.Client{}
if insecure {
fmt.Fprintln(os.Stderr, "[!] Warning: TLS verification disabled for RDAP (--insecure). Results are weaker.")
client.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
}
now := time.Now()
var target, sus domainResult
var fg errgroup.Group
fg.Go(func() error {
target = fetchDomain(ctx, client, legit, now)
target = fetchDomain(ctx, client, legit, now, perCall)
return nil
})
fg.Go(func() error {
sus = fetchDomain(ctx, client, suspect, now)
sus = fetchDomain(ctx, client, suspect, now, perCall)
return nil
})
_ = fg.Wait()
if err := score.VerifyDataQuality(target.info, sus.info); err != nil {
fmt.Fprintf(os.Stderr, "[!] Error: %v.\n", err)
msg := fmt.Sprintf("[!] Error: %v.", err)
var causes []string
if target.rdapErr != nil {
causes = append(causes, fmt.Sprintf("target RDAP: %v", target.rdapErr))
}
if target.certErr != nil {
causes = append(causes, fmt.Sprintf("target TLS: %v", target.certErr))
}
if sus.rdapErr != nil {
causes = append(causes, fmt.Sprintf("suspect RDAP: %v", sus.rdapErr))
}
if sus.certErr != nil {
causes = append(causes, fmt.Sprintf("suspect TLS: %v", sus.certErr))
}
if len(causes) > 0 {
msg += fmt.Sprintf(" Cause(s): %s.", strings.Join(causes, "; "))
}
fmt.Fprintln(os.Stderr, msg)
return 1
}
result := score.Score(target.info, sus.info)

219
compile.go Normal file
View file

@ -0,0 +1,219 @@
// Command compile is the build driver for isthislegit. It cross-compiles
// ./cmd/isthislegit for the requested platform/arch matrix:
//
// go run ./compile.go --platform=linux --arch=amd64 --static=true
//
// Static builds (the default) are CGO-free so every target compiles
// without a cross C toolchain. Dynamic builds (--static=false) enable
// CGO and link libc dynamically; they require a C compiler for the
// target (gcc for host builds, or CC set to a cross compiler).
package main
import (
"flag"
"fmt"
"os"
"os/exec"
"runtime"
"strings"
)
// platformAliases maps friendly platform names to GOOS values.
// "bsd" fans out to freebsd, openbsd, and netbsd.
var platformAliases = map[string][]string{
"linux": {"linux"},
"darwin": {"darwin"},
"mac": {"darwin"},
"macos": {"darwin"},
"osx": {"darwin"},
"windows": {"windows"},
"win": {"windows"},
"bsd": {"freebsd", "openbsd", "netbsd"},
"freebsd": {"freebsd"},
"openbsd": {"openbsd"},
"netbsd": {"netbsd"},
}
// archAlias resolves a friendly arch name to GOARCH plus optional GOARM.
type archAlias struct {
goarch string
goarm string
}
var archAliases = map[string]archAlias{
"amd64": {goarch: "amd64"},
"x64": {goarch: "amd64"},
"x86-64": {goarch: "amd64"},
"386": {goarch: "386"},
"x86": {goarch: "386"},
"i386": {goarch: "386"},
"32bit": {goarch: "386"},
"arm64": {goarch: "arm64"},
"aarch64": {goarch: "arm64"},
"arm": {goarch: "arm", goarm: "7"},
"armv7": {goarch: "arm", goarm: "7"},
"armv6": {goarch: "arm", goarm: "6"},
"armv5": {goarch: "arm", goarm: "5"},
}
// target is one GOOS/GOARCH(/GOARM) build.
type target struct {
goos string
goarch string
goarm string
}
func resolvePlatforms(raw string) ([]string, error) {
gooses, ok := platformAliases[strings.ToLower(strings.TrimSpace(raw))]
if !ok {
names := make([]string, 0, len(platformAliases))
for name := range platformAliases {
names = append(names, name)
}
return nil, fmt.Errorf("unknown platform %q (valid: %s)", raw, strings.Join(names, ", "))
}
return gooses, nil
}
func resolveArch(raw string) (archAlias, error) {
name := strings.ToLower(strings.TrimSpace(raw))
if a, ok := archAliases[name]; ok {
return a, nil
}
return archAlias{}, fmt.Errorf("unknown arch %q (valid examples: amd64, x64, arm64, aarch64, 386, x86, arm, armv7)", raw)
}
// supported pairs "goos/goarch" from the local toolchain.
func supportedPairs() (map[string]bool, error) {
out, err := exec.Command("go", "tool", "dist", "list").Output()
if err != nil {
return nil, fmt.Errorf("cannot list toolchain targets: %w", err)
}
pairs := make(map[string]bool)
for _, line := range strings.Split(string(out), "\n") {
if line = strings.TrimSpace(line); line != "" {
pairs[line] = true
}
}
return pairs, nil
}
func outputName(t target) string {
name := fmt.Sprintf("isthislegit-%s-%s", t.goos, t.goarch)
if t.goarm != "" {
name += "v" + t.goarm
}
if t.goos == "windows" {
name += ".exe"
}
return name
}
// isHost reports whether t matches the machine running compile.go.
func (t target) isHost() bool {
if t.goos != runtime.GOOS || t.goarch != runtime.GOARCH {
return false
}
if t.goarch == "arm" {
return t.goarm == goarmHost()
}
return true
}
// goarmHost reads the host GOARM (empty unless arm, where it defaults to 7).
func goarmHost() string {
if arm := os.Getenv("GOARM"); arm != "" {
return arm
}
return "7"
}
func build(t target, cgo bool) error {
// Both modes strip symbol tables and DWARF via the compiler; the
// linkage (static vs dynamic) comes from CGO_ENABLED alone.
args := []string{"build", `-ldflags=-s -w`}
cgoEnabled := "0"
if cgo {
cgoEnabled = "1"
}
args = append(args, "-o", outputName(t), "./cmd/isthislegit")
cmd := exec.Command("go", args...)
cmd.Env = append(os.Environ(),
"GOOS="+t.goos,
"GOARCH="+t.goarch,
"CGO_ENABLED="+cgoEnabled,
)
if t.goarm != "" {
cmd.Env = append(cmd.Env, "GOARM="+t.goarm)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
fmt.Printf("[*] GOOS=%s GOARCH=%s CGO_ENABLED=%s", t.goos, t.goarch, cgoEnabled)
if t.goarm != "" {
fmt.Printf(" GOARM=%s", t.goarm)
}
fmt.Printf(" -> %s\n", outputName(t))
if err := cmd.Run(); err != nil {
return fmt.Errorf("build %s/%s failed: %w", t.goos, t.goarch, err)
}
return nil
}
func run() int {
platformFlag := flag.String("platform", "linux", "target OS: linux, mac/darwin, windows/win, bsd (freebsd+openbsd+netbsd), or a single BSD")
archFlag := flag.String("arch", "amd64", "target arch: amd64/x64, arm64/aarch64, 386/x86, arm/armv7, ...")
staticFlag := flag.Bool("static", true, "true for static (CGO_ENABLED=0), false for dynamic (CGO_ENABLED=1, host builds); both stripped via -ldflags")
flag.Parse()
gooses, err := resolvePlatforms(*platformFlag)
if err != nil {
fmt.Fprintf(os.Stderr, "[!] Error: %v.\n", err)
return 1
}
arch, err := resolveArch(*archFlag)
if err != nil {
fmt.Fprintf(os.Stderr, "[!] Error: %v.\n", err)
return 1
}
pairs, err := supportedPairs()
if err != nil {
fmt.Fprintf(os.Stderr, "[!] Error: %v.\n", err)
return 1
}
var targets []target
for _, goos := range gooses {
if !pairs[goos+"/"+arch.goarch] {
fmt.Fprintf(os.Stderr, "[!] Error: target %s/%s is not supported by this toolchain.\n", goos, arch.goarch)
return 1
}
targets = append(targets, target{goos: goos, goarch: arch.goarch, goarm: arch.goarm})
}
// Dynamic builds need a C compiler for the target: gcc covers host
// builds, while cross builds require CC set to a cross compiler.
if !*staticFlag && os.Getenv("CC") == "" {
for _, t := range targets {
if !t.isHost() {
fmt.Fprintf(os.Stderr, "[!] Error: --static=false needs CGO, which cannot cross-compile to %s/%s without a cross C compiler (set CC or use --static=true).\n", t.goos, t.goarch)
return 1
}
}
}
built := 0
for _, t := range targets {
if err := build(t, !*staticFlag); err != nil {
fmt.Fprintf(os.Stderr, "[!] Error: %v.\n", err)
return 1
}
built++
}
fmt.Printf("[+] Built %d of %d target(s).\n", built, len(targets))
return 0
}
func main() {
os.Exit(run())
}

View file

@ -30,14 +30,15 @@ dns.ValidSyntax("no-dot") // =&gt; false</pre></td></tr></table>
<hr>
<a name="CheckDNS"></a>
<font color="#859900"><h2>CheckDNS</h2></font>
<table border="0" cellpadding="8" width="100%"><tr><td bgcolor="#073642"><pre>func CheckDNS(ctx context.Context, domain string) error</pre></td></tr></table>
<table border="0" cellpadding="8" width="100%"><tr><td bgcolor="#073642"><pre>func CheckDNS(ctx context.Context, domain string, timeout time.Duration) error</pre></td></tr></table>
<p>Aborts early on NXDOMAIN or unresolvable names via
<tt>net.DefaultResolver.LookupIP</tt> with a 5 second deadline. Returns
<tt>net.DefaultResolver.LookupIP</tt> with the given deadline. Returns
<tt>nil</tt> when at least one IPv4 address resolves, an error otherwise.</p>
<table border="1" cellpadding="4">
<tr><th>Parameter</th><th>Description</th></tr>
<tr><td><tt>ctx</tt></td><td>Parent context; the global 15 second budget.</td></tr>
<tr><td><tt>domain</tt></td><td>Already-normalized domain to resolve.</td></tr>
<tr><td><tt>timeout</tt></td><td>Per-call deadline (the CLI --timeout value).</td></tr>
</table>
</body>
</html>

View file

@ -12,17 +12,20 @@ symbols; this page documents behavior.</p>
<font color="#859900"><h2>Arguments</h2></font>
<p>Syntax: <tt>isthislegit [FLAGS] &lt;legit_domain&gt;
&lt;suspect_domain&gt;</tt>. Flags <tt>-v/--verbose</tt>,
<tt>-j/--json</tt>, <tt>--scale</tt>, <tt>-h/--help</tt>, plus a
<tt>-j/--json</tt>, <tt>-k/--insecure</tt> (skips RDAP TLS verification with
a warning), <tt>--timeout=DUR</tt> (Go duration, default 5s),
<tt>--scale</tt>, <tt>-h/--help</tt>, plus a
<tt>--</tt> separator. Unknown flags and anything other than exactly two
positional domains exit 1 with a usage message. Domains are normalized with
<a href="dns.html#NormalizeDomain">dns.NormalizeDomain</a> and validated
with <a href="dns.html#ValidSyntax">dns.ValidSyntax</a>.</p>
with <a href="dns.html#ValidSyntax">dns.ValidSyntax</a>. Fail-closed errors
include the underlying fetch cause(s).</p>
<font color="#859900"><h2>Budgets</h2></font>
<table border="1" cellpadding="4">
<tr><th>Budget</th><th>Value</th></tr>
<tr><td>Global deadline (both domains)</td><td>15 seconds</td></tr>
<tr><td>Per-call deadline (DNS, TLS, RDAP)</td><td>5 seconds</td></tr>
<tr><td>Per-call deadline (DNS, TLS, RDAP)</td><td>5 seconds, adjustable with --timeout</td></tr>
<tr><td>Attempts per call</td><td>3, with 1 second between retries</td></tr>
</table>

View file

@ -23,6 +23,8 @@ go test ./...</pre></td></tr></table>
<tr><th>Flag</th><th>Description</th></tr>
<tr><td><tt>-v, --verbose</tt></td><td>Append raw cert policy OIDs, TLS details, and RDAP JSON.</td></tr>
<tr><td><tt>-j, --json</tt></td><td>Output the result summary as JSON for scripting.</td></tr>
<tr><td><tt>-k, --insecure</tt></td><td>Skip TLS verification for RDAP (weakens results; last resort for devices without CA certificates).</td></tr>
<tr><td><tt>--timeout=DUR</tt></td><td>Per-call deadline for DNS/TLS/RDAP; Go duration, default 5s.</td></tr>
<tr><td><tt>--scale</tt></td><td>Print the scoring scale table and exit.</td></tr>
<tr><td><tt>-h, --help</tt></td><td>Display usage and exit.</td></tr>
</table>
@ -30,8 +32,10 @@ go test ./...</pre></td></tr></table>
<font color="#859900"><h2>Fetch Pipeline</h2></font>
<p>Both domains are fetched concurrently with
<tt>golang.org/x/sync/errgroup</tt> under a 15 second global deadline; each
DNS, TLS, and RDAP call carries a 5 second deadline and is retried up to 3
times. Total run time equals the slower of the two domains.</p>
DNS, TLS, and RDAP call carries a 5 second deadline (adjustable with
<tt>--timeout</tt>) and is retried up to 3 times. Total run time equals the
slower of the two domains. Fail-closed errors name the underlying
cause(s).</p>
<table border="0" cellpadding="8" width="100%"><tr><td bgcolor="#073642"><pre>parse args --&gt; DNS pre-flight (target + suspect, parallel)
--&gt; fetch target = RDAP + TLS in parallel
--&gt; fetch suspect = RDAP + TLS in parallel
@ -40,6 +44,14 @@ times. Total run time equals the slower of the two domains.</p>
<tt>--help</tt>), <tt>1</tt> on bad flags, invalid domains, DNS failure, or
failed data-quality verification.</p>
<font color="#859900"><h2>Embedded Systems</h2></font>
<p>The tool runs on embedded ARM devices (verified on a Miyoo Mini Plus),
but minimal firmware often ships without a CA certificate store. DNS and
certificate inspection still work there, while verified HTTPS (RDAP) fails;
the fail-closed error names the cause (<tt>x509: certificate signed by
unknown authority</tt>). Install <tt>ca-certificates</tt> and fix the clock
where possible, otherwise use <tt>-k/--insecure</tt> as a last resort.</p>
<font color="#859900"><h2>Scoring Scale</h2></font>
<table border="1" cellpadding="4">
<tr><th>Signal</th><th>Points</th></tr>

View file

@ -38,8 +38,8 @@ func ValidSyntax(d string) bool {
// CheckDNS aborts early on NXDOMAIN / unresolvable names.
// Mirrors bash check_dns (dig +short A). Uses the stdlib resolver.
func CheckDNS(ctx context.Context, domain string) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
func CheckDNS(ctx context.Context, domain string, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
addrs, err := net.DefaultResolver.LookupIP(ctx, "ip4", domain)
if err != nil {