├── .gitignore ├── internal ├── ui │ ├── icon │ │ ├── data │ │ │ ├── menubar.png │ │ │ └── fingertip.ico │ │ ├── icon_darwin.go │ │ ├── icon_linux.go │ │ └── icon_windows.go │ ├── dialog.go │ ├── dialog_linux.go │ └── tray.go ├── resolvers │ ├── proc │ │ ├── proc_posix.go │ │ ├── proc_windows.go │ │ └── hns.go │ ├── cache.go │ ├── util_test.go │ ├── dnssec │ │ ├── testdata │ │ │ ├── val_rhybar.cz.txt │ │ │ ├── val_huque.com.txt │ │ │ ├── val_cloudflare.com.txt │ │ │ ├── val_example.com.txt │ │ │ ├── val_ns.forever.txt │ │ │ └── val_hns.txt │ │ ├── dnssec_test.go │ │ └── dnssec.go │ ├── util.go │ ├── ethereum.go │ ├── hip5_test.go │ └── hip5.go └── config │ ├── onboarding.go │ ├── auto │ ├── install_linux.go │ ├── install.go │ ├── install_darwin.go │ └── install_windows.go │ ├── user.go │ ├── pages │ ├── setup.html │ └── index.html │ ├── debug.go │ └── config.go ├── go.mod ├── .github └── workflows │ └── build-linux.yml ├── README.md ├── main.go └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | rsrc.syso -------------------------------------------------------------------------------- /internal/ui/icon/data/menubar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imperviousinc/fingertip/HEAD/internal/ui/icon/data/menubar.png -------------------------------------------------------------------------------- /internal/ui/icon/data/fingertip.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imperviousinc/fingertip/HEAD/internal/ui/icon/data/fingertip.ico -------------------------------------------------------------------------------- /internal/ui/icon/icon_darwin.go: -------------------------------------------------------------------------------- 1 | //+build darwin 2 | 3 | package icon 4 | 5 | import _ "embed" 6 | 7 | //go:embed data/menubar.png 8 | var Toolbar []byte 9 | -------------------------------------------------------------------------------- /internal/ui/icon/icon_linux.go: -------------------------------------------------------------------------------- 1 | //+build linux 2 | 3 | package icon 4 | 5 | import _ "embed" 6 | 7 | //go:embed data/fingertip.ico 8 | var Toolbar []byte 9 | -------------------------------------------------------------------------------- /internal/ui/icon/icon_windows.go: -------------------------------------------------------------------------------- 1 | //+build windows 2 | 3 | package icon 4 | 5 | import _ "embed" 6 | 7 | //go:embed data/fingertip.ico 8 | var Toolbar []byte 9 | -------------------------------------------------------------------------------- /internal/resolvers/proc/proc_posix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package proc 4 | 5 | import ( 6 | "syscall" 7 | ) 8 | 9 | var processAttributes = &syscall.SysProcAttr{} 10 | var processExtension = "" 11 | -------------------------------------------------------------------------------- /internal/resolvers/proc/proc_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package proc 4 | 5 | import ( 6 | "syscall" 7 | ) 8 | 9 | var processAttributes = &syscall.SysProcAttr{HideWindow: true} 10 | var processExtension = ".exe" 11 | -------------------------------------------------------------------------------- /internal/ui/dialog.go: -------------------------------------------------------------------------------- 1 | //+build !linux 2 | 3 | package ui 4 | 5 | import "github.com/sqweek/dialog" 6 | 7 | func ShowErrorDlg(err string) { 8 | dialog.Message("%s", err).Title("Error").Error() 9 | } 10 | 11 | func ShowYesNoDlg(msg string) bool { 12 | return dialog.Message("%s", msg).Title("Confirmation").YesNo() 13 | } 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module fingertip 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/buffrr/letsdane v0.6.1 7 | github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a 8 | github.com/ethereum/go-ethereum v1.10.8 9 | github.com/getlantern/systray v1.1.0 10 | github.com/miekg/dns v1.1.31 11 | github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 12 | github.com/spf13/viper v1.8.1 13 | github.com/sqweek/dialog v0.0.0-20210702151303-c326b49d3f01 14 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 15 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d 16 | golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34 17 | ) 18 | -------------------------------------------------------------------------------- /internal/ui/dialog_linux.go: -------------------------------------------------------------------------------- 1 | //+build linux 2 | 3 | package ui 4 | 5 | import ( 6 | "os/exec" 7 | ) 8 | 9 | var tool string 10 | 11 | func init() { 12 | for _, tool = range []string{"qarma", "zenity", "matedialog"} { 13 | path, _ := exec.LookPath(tool) 14 | if path != "" { 15 | return 16 | } 17 | } 18 | tool = "zenity" 19 | } 20 | 21 | func run(args []string) ([]byte, error) { 22 | return exec.Command(tool, args...).Output() 23 | } 24 | 25 | func ShowErrorDlg(err string) { 26 | run([]string{"--no-wrap", "--error", "--text", err}) 27 | } 28 | 29 | func ShowYesNoDlg(msg string) bool { 30 | _, err := run([]string{"--no-wrap", "--question", "--text", msg}) 31 | return err == nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/config/onboarding.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | _ "embed" 5 | "html/template" 6 | ) 7 | 8 | type onBoardingTmplData struct { 9 | NavSetupLink string 10 | NavStatusLink string 11 | CertPath string 12 | CertLink string 13 | PACLink string 14 | Version string 15 | } 16 | 17 | //go:embed pages/index.html 18 | var statusPage string 19 | 20 | //go:embed pages/setup.html 21 | var setupPage string 22 | 23 | var setupTmpl *template.Template 24 | var statusTmpl *template.Template 25 | 26 | func init() { 27 | var err error 28 | if setupTmpl, err = template.New("setup").Parse(setupPage); err != nil { 29 | panic(err) 30 | } 31 | if statusTmpl, err = template.New("status").Parse(statusPage); err != nil { 32 | panic(err) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/config/auto/install_linux.go: -------------------------------------------------------------------------------- 1 | //+build linux 2 | 3 | package auto 4 | 5 | import ( 6 | "crypto/x509" 7 | "errors" 8 | ) 9 | 10 | var relativeProfilesPath = "Mozilla/Firefox/Profiles" 11 | 12 | func VerifyCert(certPath string) (err error) { 13 | var cert *x509.Certificate 14 | if cert, err = readX509Cert(certPath); err != nil { 15 | return 16 | } 17 | 18 | _, err = cert.Verify(x509.VerifyOptions{}) 19 | return 20 | } 21 | 22 | // Supported whether auto configuration 23 | // is supported for this build 24 | func Supported() bool { 25 | return false 26 | } 27 | 28 | func UninstallAutoProxy(autoURL string) { 29 | return 30 | } 31 | 32 | func InstallAutoProxy(autoURL string) error { 33 | return errors.New("not supported") 34 | } 35 | 36 | func InstallCert(certPath string) error { 37 | return errors.New("not supported") 38 | } 39 | 40 | func UninstallCert(certPath string) error { 41 | return errors.New("not supported") 42 | } 43 | -------------------------------------------------------------------------------- /internal/resolvers/cache.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type entry struct { 9 | msg interface{} 10 | ttl time.Time 11 | } 12 | 13 | type cache struct { 14 | m map[string]*entry 15 | maxN int 16 | 17 | sync.RWMutex 18 | } 19 | 20 | func newCache(maxN int) (m *cache) { 21 | return &cache{m: make(map[string]*entry), maxN: maxN} 22 | } 23 | 24 | func (c *cache) set(key string, item *entry) { 25 | c.Lock() 26 | defer c.Unlock() 27 | 28 | if c.maxN == len(c.m) { 29 | for k := range c.m { 30 | delete(c.m, k) 31 | break 32 | } 33 | } 34 | 35 | c.m[key] = item 36 | } 37 | 38 | func (c *cache) get(key string) (*entry, bool) { 39 | c.RLock() 40 | defer c.RUnlock() 41 | 42 | i, ok := c.m[key] 43 | return i, ok 44 | } 45 | 46 | func (c *cache) remove(key string) { 47 | c.Lock() 48 | defer c.Unlock() 49 | 50 | delete(c.m, key) 51 | } 52 | 53 | func (c *cache) len() int { 54 | c.RLock() 55 | defer c.RUnlock() 56 | 57 | return len(c.m) 58 | } 59 | -------------------------------------------------------------------------------- /internal/resolvers/util_test.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "github.com/miekg/dns" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestLastNLabels(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | n int 13 | out string 14 | }{ 15 | { 16 | name: "example.com", 17 | n: 1, 18 | out: "com", 19 | }, 20 | { 21 | name: "www.test.example.", 22 | n: 2, 23 | out: "test.example", 24 | }, 25 | { 26 | name: "www.example.FOO.", 27 | n: 5, 28 | out: "www.example.foo", 29 | }, 30 | } 31 | 32 | for _, test := range tests { 33 | t.Run(test.name, func(t *testing.T) { 34 | out := LastNLabels(test.name, test.n) 35 | if out != test.out { 36 | t.Fatalf("got %s, want %s", out, test.out) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func Test_getTTL(t *testing.T) { 43 | if ttl := getTTL([]dns.RR{ 44 | testRR("example. 300 IN A 127.0.0.1"), 45 | }); ttl != 300*time.Second { 46 | t.Fatalf("got ttl = %v, want %v", ttl, 300*time.Second) 47 | } 48 | 49 | if ttl := getTTL([]dns.RR{ 50 | testRR("example. 5 IN A 127.0.0.1"), 51 | }); ttl != time.Minute { 52 | t.Fatalf("got ttl = %v, want %v", ttl, time.Minute) 53 | } 54 | 55 | if ttl := getTTL([]dns.RR{ 56 | testRR("example. 0 IN A 127.0.0.1"), 57 | testRR("example. 300 IN A 127.0.0.1"), 58 | }); ttl != time.Minute { 59 | t.Fatalf("got ttl = %v, want %v", ttl, time.Minute) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/resolvers/dnssec/testdata/val_rhybar.cz.txt: -------------------------------------------------------------------------------- 1 | [ZONE] origin: rhybar.cz., time: 20210823000000 2 | [TRUST_ANCHORS] 3 | rhybar.cz. 3600 IN DS 61281 13 2 3651AA62843DEB5EDAA10F5055717F877E9F2657E701CAB10CF42B43 EA8E047E 4 | 5 | ; ignore verification for dnskeys 6 | [DNSKEYS] verify: 0 7 | ;; OPT PSEUDOSECTION: 8 | ; EDNS: version: 0, flags: do; udp: 1232 9 | ;; QUESTION SECTION: 10 | ;rhybar.cz. IN DNSKEY 11 | 12 | ;; ANSWER SECTION: 13 | rhybar.cz. 600 IN DNSKEY 256 3 5 AwEAAb/riVUjNfP1to3wkJyul0MjwiPojFgFmMiLj1KIKeVIYCIRNx01 Q1we5M17GQFInCXXyTyjCYJfwkL0Xe7ma6m2pHfEMkOiDl42rsgrmkSh xPEvZMd5vpT+RyQWQh26TJ42MRoCJSt6XNeFLXRyjfRcDt7ZxYD3bHNe yaDuUUGt 14 | rhybar.cz. 600 IN DNSKEY 256 3 5 AwEAAcrTMVXwOcFCGKtXwdt4XATP43qU96IryyqiZ0oPtuHEEBCikuQD uJhRjNAV4DYvR6fb/suAnd91EVNgHHTXUlAWwmJRrqIwZ6VuGaZqVG+N Jh1Okif7CL8no2Z47j6I3HH3pyzrYH2oQVyr64O/8BV2jrk8RteeEqa7 V7gcrFfJ 15 | rhybar.cz. 600 IN DNSKEY 257 3 5 AwEAAeKle4K3bxJb4k9sMhdm6BmpRK2rISAGh0egMSXgOlQnU+3TLQ0a H1th7ejZnn6Zdkeo8MRXDxLkgp1rUSsRM1Q2SmLJhaat7L15qHmj+vCk 5IuSIpAdaRsqOKxHlT6a/LWGwGvDIVxY6J9sXaJ4SInflZpa5wZUCrhD Kvpo0hAzNfoK/aFApzZGaAGALYx6YpbG+SBW2K+s92eyoJCCrQQ+Nata 41l7K6RFAYjP+g3Kp95McNm3xlBve171u9FUZNUuN2Rn25oEtHHlK9Nc HNqWvFJ3VmXcA6CkGrBPV6vOAwwUtPDSWSZbdolS69092ZWYTlOJw6g0 LVI2feMMrok= 16 | 17 | [TEST_BEGIN] name: nodata without rrsigs 18 | [INPUT] 19 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 34338 20 | ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 2, ADDITIONAL: 1 21 | 22 | ;; OPT PSEUDOSECTION: 23 | ; EDNS: version: 0, flags: do; udp: 1232 24 | ;; QUESTION SECTION: 25 | ;rhybar.cz. IN A 26 | 27 | ;; AUTHORITY SECTION: 28 | rhybar.cz. 473 IN SOA a.ns.nic.cz. hostmaster.nic.cz. 2008091210 10800 3600 1209600 7200 29 | rhybar.cz. 7073 IN NSEC www.rhybar.cz. NS SOA RRSIG NSEC DNSKEY 30 | [RESULT] secure: 0, bogus: 1 31 | [TEST_END] 32 | -------------------------------------------------------------------------------- /.github/workflows/build-linux.yml: -------------------------------------------------------------------------------- 1 | name: Build and Package - Linux 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build-hnsd: 7 | runs-on: ubuntu-18.04 8 | 9 | steps: 10 | - name: Checkout hnsd repository 11 | uses: actions/checkout@v3 12 | with: 13 | repository: 'handshake-org/hnsd' 14 | 15 | - name: Install dependencies 16 | run: sudo apt install -y libunbound-dev 17 | 18 | - name: Build hnsd 19 | run: | 20 | ./autogen.sh && ./configure && make 21 | ls -l 22 | 23 | - name: Store hnsd binary 24 | uses: actions/upload-artifact@v3 25 | with: 26 | name: hnsd-bin 27 | path: ./hnsd 28 | 29 | build-fingertip: 30 | needs: build-hnsd 31 | runs-on: ubuntu-18.04 32 | 33 | steps: 34 | - name: Checkout repository 35 | uses: actions/checkout@v3 36 | 37 | - name: Install go 38 | uses: actions/setup-go@v3 39 | with: 40 | go-version: '1.17' 41 | cache: true # Cache go modules 42 | 43 | - name: Install dependencies 44 | run: sudo apt install -y libgtk-3-dev libappindicator3-dev libunbound-dev 45 | 46 | - name: Build fingertip 47 | run: | 48 | go build -trimpath -o ./builds/linux/appdir/usr/bin 49 | ls -l builds/linux/appdir/usr/bin/ 50 | 51 | - name: Download hnsd binary 52 | uses: actions/download-artifact@v3 53 | with: 54 | name: hnsd-bin 55 | path: builds/linux/appdir/usr/bin 56 | 57 | - name: Package as AppImage 58 | working-directory: ./builds/linux 59 | run: | 60 | ls -l appdir/usr/bin/ 61 | chmod +x appdir/usr/bin/hnsd 62 | wget -c -nv "https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage" 63 | chmod a+x linuxdeployqt-continuous-x86_64.AppImage 64 | ./linuxdeployqt-continuous-x86_64.AppImage appdir/usr/share/applications/fingertip.desktop -appimage -executable=appdir/usr/bin/hnsd 65 | 66 | - name: Store fingertip binary 67 | uses: actions/upload-artifact@v3 68 | with: 69 | name: fingertip-bin 70 | path: ./builds/linux/appdir/usr/bin/fingertip 71 | 72 | - name: Store fingertip appimage 73 | uses: actions/upload-artifact@v3 74 | with: 75 | name: fingertip-appimage 76 | path: ./builds/linux/Fingertip*.AppImage 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fingertip 2 | 3 | **Note:** This project is experimental use at your own risk. 4 | 5 | Fingertip is a menubar app that runs a [lightweight decentralized resolver](https://github.com/handshake-org/hnsd) to resolve names from the [Handshake](https://handshake.org) root zone. It can also resolve names from external namespaces such as the Ethereum Name System. Fingertip integrates with [letsdane](https://github.com/buffrr/letsdane) to provide TLS support without relying on a centralized certificate authority. 6 | 7 | 8 | 9 | 10 | ## Install 11 | 12 | You can use a pre-built binary from releases or build your own from source. 13 | 14 | ## Configuration 15 | You can set these as environment variables prefixed with `FINGERTIP_` or store it in the app config directory as `fingertip.env` 16 | 17 | ``` 18 | # letsdane proxy address 19 | PROXY_ADDRESS=127.0.0.1:9590 20 | # hnsd root server address 21 | ROOT_ADDRESS=127.0.0.1:9591 22 | # hnsd recursive resolver address 23 | RECURSIVE_ADDRESS=127.0.0.1:9592 24 | # Connect your own Ethereum full node/or blockchain provider such as Infura 25 | #ETHEREUM_ENDPOINT=/home/user/.ethereum/geth.ipc or 26 | #ETHEREUM_ENDPOINT=https://mainnet.infura.io/v3/YOUR-PROJECT-ID 27 | ``` 28 | 29 | ## Build from source 30 | 31 | Go 1.16+ is required. 32 | 33 | ``` 34 | $ git clone https://github.com/buffrr/fingertip 35 | ``` 36 | 37 | ### MacOS 38 | 39 | ``` 40 | $ brew install dylibbundler git automake autoconf libtool unbound 41 | $ git clone https://github.com/imperviousinc/fingertip 42 | $ cd fingertip && ./builds/macos/build.sh 43 | ``` 44 | 45 | For development, you can run fingertip from the following path: 46 | ``` 47 | $ ./builds/macos/Fingertip.app/Contents/MacOS/fingertip 48 | ``` 49 | 50 | Configure your IDE to output to this directory or continue to use `build.sh` when making changes (it will only build hnsd once). 51 | 52 | ### Windows 53 | 54 | Follow [hnsd](https://github.com/handshake-org/hnsd) build instructions for windows. Copy hnsd.exe binary and its dependencies (libcrypto, libssl and libunbound dlls) into the `fingertip/builds/windows` directory. 55 | You no longer need to use MSYS shell. 56 | 57 | ``` 58 | $ choco install mingw 59 | $ go build -trimpath -o ./builds/windows/ -ldflags "-H windowsgui" 60 | ``` 61 | 62 | ### Linux 63 | 64 | Follow [hnsd](https://github.com/handshake-org/hnsd) build instructions for Linux. Copy hnsd binary into the `fingertip/builds/linux/appdir/usr/bin` directory. 65 | 66 | ``` 67 | $ go build -trimpath -o ./builds/linux/appdir/usr/bin/ 68 | ``` 69 | 70 | 71 | ## Credits 72 | Fingertip uses [hnsd](https://github.com/handshake-org/hnsd) a lightweight Handshake resolver, [letsdane](https://github.com/buffrr/letsdane) for TLS support and [go-ethereum](https://github.com/ethereum/go-ethereum) for .eth and Ethereum [HIP-5](https://github.com/handshake-org/HIPs/blob/master/HIP-0005.md) lookups. 73 | 74 | The name "fingertip" was stolen from [@pinheadmz](https://github.com/pinheadmz) 75 | -------------------------------------------------------------------------------- /internal/config/user.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/spf13/viper" 8 | "io/ioutil" 9 | "os" 10 | ) 11 | 12 | const ( 13 | DefaultProxyAddr = "127.0.0.1:9590" 14 | DefaultRootAddr = "127.0.0.1:9591" 15 | DefaultRecursiveAddr = "127.0.0.1:9592" 16 | DefaultEthereumEndpoint = "https://mainnet.infura.io/v3/b0933ce6026a4e1e80e89e96a5d095bc" 17 | ) 18 | 19 | // User Represents user facing configuration 20 | type User struct { 21 | ProxyAddr string `mapstructure:"PROXY_ADDRESS"` 22 | RootAddr string `mapstructure:"ROOT_ADDRESS"` 23 | RecursiveAddr string `mapstructure:"RECURSIVE_ADDRESS"` 24 | EthereumEndpoint string `mapstructure:"ETHEREUM_ENDPOINT"` 25 | } 26 | 27 | // Stored config 28 | type Store struct { 29 | Version string `json:"version"` 30 | AutoConfig bool `json:"auto_config"` 31 | 32 | path string 33 | } 34 | 35 | func readStore(path, version string, old *Store) (*Store, error) { 36 | var zero *Store 37 | if old != nil { 38 | zero = old 39 | } else { 40 | zero = &Store{ 41 | AutoConfig: false, 42 | Version: version, 43 | path: path, 44 | } 45 | } 46 | if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { 47 | return zero, nil 48 | } 49 | 50 | b, err := ioutil.ReadFile(path) 51 | if err != nil { 52 | return nil, fmt.Errorf("failed reading app config: %v", err) 53 | } 54 | if len(b) == 0 { 55 | return zero, nil 56 | } 57 | if err := json.Unmarshal(b, zero); err != nil { 58 | return nil, fmt.Errorf("failed parsing app config: %v", err) 59 | } 60 | return zero, nil 61 | } 62 | 63 | func (i *Store) Reload() error { 64 | _, err := readStore(i.path, i.Version, i) 65 | return err 66 | } 67 | 68 | func (i *Store) Save() error { 69 | b, err := json.Marshal(i) 70 | if err != nil { 71 | return fmt.Errorf("failed encoding app config: %v", err) 72 | } 73 | 74 | if err := ioutil.WriteFile(i.path, b, 0664); err != nil { 75 | return fmt.Errorf("faild writing app config: %v", err) 76 | } 77 | return err 78 | } 79 | 80 | var ErrUserConfigNotFound = errors.New("user config not found") 81 | 82 | // ReadUserConfig reads user facing configuration 83 | func ReadUserConfig(path string) (config User, err error) { 84 | // TODO: Viper is likely overkill write a custom loader 85 | viper.AddConfigPath(path) 86 | viper.SetConfigName("fingertip.env") 87 | viper.SetConfigType("env") 88 | viper.SetEnvPrefix("FINGERTIP") 89 | viper.AutomaticEnv() 90 | 91 | viper.SetDefault("PROXY_ADDRESS", DefaultProxyAddr) 92 | viper.SetDefault("ROOT_ADDRESS", DefaultRootAddr) 93 | viper.SetDefault("RECURSIVE_ADDRESS", DefaultRecursiveAddr) 94 | viper.SetDefault("ETHEREUM_ENDPOINT", DefaultEthereumEndpoint) 95 | 96 | err = viper.ReadInConfig() 97 | if err != nil { 98 | if _, ok := err.(viper.ConfigFileNotFoundError); !ok { 99 | err = fmt.Errorf("error reading user config: %v", err) 100 | return 101 | } 102 | } 103 | 104 | err = viper.Unmarshal(&config) 105 | if err != nil { 106 | err = fmt.Errorf("error reading user config: %v", err) 107 | } 108 | return 109 | } 110 | -------------------------------------------------------------------------------- /internal/ui/tray.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fingertip/internal/config" 5 | "fingertip/internal/ui/icon" 6 | "fmt" 7 | "github.com/getlantern/systray" 8 | "sync" 9 | ) 10 | 11 | var ( 12 | OnExit func() 13 | OnStart func() 14 | OnAutostart func(checked bool) bool 15 | OnConfigureOS func(checked bool) bool 16 | OnOpenHelp func() 17 | OnStop func() 18 | OnReady func() 19 | Data State 20 | ) 21 | 22 | func Loop() { 23 | systray.Run(Data.initMenu, OnExit) 24 | } 25 | 26 | type State struct { 27 | started bool 28 | runToggle *systray.MenuItem 29 | openAtLogin *systray.MenuItem 30 | blockHeight *systray.MenuItem 31 | options *systray.MenuItem 32 | quit *systray.MenuItem 33 | 34 | autoConfig *systray.MenuItem 35 | openSetup *systray.MenuItem 36 | 37 | sync.RWMutex 38 | } 39 | 40 | // space padding for width 41 | var startTitle = fmt.Sprintf("%-35s", "Start") 42 | var stopTitle = fmt.Sprintf("%-35s", "Stop") 43 | 44 | func (s *State) SetBlockHeight(h string) { 45 | s.Lock() 46 | defer s.Unlock() 47 | if s.blockHeight == nil { 48 | return 49 | } 50 | s.blockHeight.SetTitle("Block height " + h) 51 | } 52 | 53 | func (s *State) Started() bool { 54 | s.RLock() 55 | defer s.RUnlock() 56 | 57 | return s.started 58 | } 59 | 60 | func (s *State) toggleStarted() bool { 61 | if s.Started() { 62 | s.SetStarted(false) 63 | return false 64 | } 65 | 66 | return true 67 | } 68 | 69 | func (s *State) SetStarted(started bool) { 70 | s.Lock() 71 | defer s.Unlock() 72 | 73 | s.started = started 74 | 75 | if s.started { 76 | s.runToggle.SetTitle(stopTitle) 77 | } else { 78 | s.runToggle.SetTitle(startTitle) 79 | s.blockHeight.SetTitle("Block height --") 80 | } 81 | } 82 | 83 | func (s *State) SetOpenAtLogin(checked bool) { 84 | if checked { 85 | s.openAtLogin.Check() 86 | return 87 | } 88 | s.openAtLogin.Uncheck() 89 | } 90 | 91 | func (s *State) OpenAtLogin() bool { 92 | return s.openAtLogin.Checked() 93 | } 94 | 95 | func (s *State) SetAutoConfig(checked bool) { 96 | if checked { 97 | s.autoConfig.Check() 98 | return 99 | } 100 | 101 | s.autoConfig.Uncheck() 102 | } 103 | 104 | func (s *State) SetAutoConfigEnabled(enabled bool) { 105 | if enabled { 106 | s.autoConfig.Enable() 107 | return 108 | } 109 | 110 | s.autoConfig.Disable() 111 | } 112 | 113 | func (s *State) SetOptionsEnabled(enabled bool) { 114 | if enabled { 115 | s.options.Enable() 116 | return 117 | } 118 | s.options.Disable() 119 | } 120 | 121 | func (s *State) initMenu() { 122 | systray.SetTemplateIcon(icon.Toolbar, icon.Toolbar) 123 | systray.SetTooltip(config.AppName) 124 | 125 | s.runToggle = systray.AddMenuItem(startTitle, "") 126 | s.openAtLogin = systray.AddMenuItemCheckbox("Open at login", "Open at login", false) 127 | 128 | systray.AddSeparator() 129 | s.blockHeight = systray.AddMenuItem("Block height --", "Block height") 130 | s.blockHeight.Disable() 131 | 132 | systray.AddSeparator() 133 | s.options = systray.AddMenuItem("Options", "") 134 | 135 | s.autoConfig = s.options.AddSubMenuItemCheckbox("Auto configure", "", false) 136 | s.openSetup = s.options.AddSubMenuItem("Help", "") 137 | 138 | s.quit = systray.AddMenuItem("Quit", "") 139 | 140 | OnReady() 141 | 142 | go func() { 143 | for { 144 | select { 145 | case <-s.runToggle.ClickedCh: 146 | if s.toggleStarted() { 147 | OnStart() 148 | continue 149 | } 150 | 151 | OnStop() 152 | case <-s.openAtLogin.ClickedCh: 153 | s.SetOpenAtLogin(OnAutostart(s.openAtLogin.Checked())) 154 | case <-s.autoConfig.ClickedCh: 155 | s.SetAutoConfig(OnConfigureOS(s.autoConfig.Checked())) 156 | case <-s.openSetup.ClickedCh: 157 | OnOpenHelp() 158 | continue 159 | case <-s.quit.ClickedCh: 160 | systray.Quit() 161 | return 162 | } 163 | } 164 | }() 165 | } 166 | -------------------------------------------------------------------------------- /internal/config/auto/install.go: -------------------------------------------------------------------------------- 1 | package auto 2 | 3 | import ( 4 | "bufio" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "errors" 8 | "fmt" 9 | "io/fs" 10 | "io/ioutil" 11 | "os" 12 | "path" 13 | "strings" 14 | ) 15 | 16 | type ProxyStatus int 17 | 18 | const ( 19 | ProxyStatusNone ProxyStatus = 0 20 | ProxyStatusInstalled = 1 21 | ProxyStatusConflict = 2 22 | ) 23 | 24 | // firefox 25 | const ( 26 | userProfileHeader = "// AUTOCONFIG:FINGERTIP" 27 | userPref = userProfileHeader + 28 | ` (autogenerated - remove this line to disable auto config for this profile and edit the file) 29 | user_pref("security.enterprise_roots.enabled", true); 30 | user_pref("network.proxy.type", 5); 31 | ` 32 | ) 33 | 34 | func equalURL(a, b string) bool { 35 | a = strings.TrimSuffix(strings.TrimSpace(a), "/") 36 | b = strings.TrimSuffix(strings.TrimSpace(b), "/") 37 | return strings.EqualFold(a, b) 38 | } 39 | 40 | // ConfigureFirefox instructs firefox to read system certs and proxy 41 | // settings 42 | func ConfigureFirefox() error { 43 | profiles, err := getProfilePaths() 44 | if err != nil { 45 | return err 46 | } 47 | 48 | var lastErr error 49 | for _, profilePath := range profiles { 50 | if err := writeUserConfig(profilePath); err != nil { 51 | lastErr = err 52 | } 53 | } 54 | return lastErr 55 | } 56 | 57 | // UndoFirefoxConfiguration undoes all actions made by ConfigureFirefox 58 | func UndoFirefoxConfiguration() { 59 | profiles, err := getProfilePaths() 60 | if err != nil { 61 | return 62 | } 63 | 64 | for _, profilePath := range profiles { 65 | userjs := path.Join(profilePath, "user.js") 66 | if ok, _, _ := fileLineContains(userjs, userProfileHeader); ok { 67 | _ = os.Remove(userjs) 68 | } 69 | } 70 | } 71 | 72 | func writeUserConfig(profilePath string) (err error) { 73 | prefs := path.Join(profilePath, "prefs.js") 74 | // if user has existing proxy configuration 75 | // ignore this profile 76 | if ok, line, _ := fileLineContains(prefs, `"network.proxy.type"`); ok { 77 | // loosely check if its type 0 (no proxy) or 5 (system proxy) 78 | if !strings.ContainsRune(line, '0') && !strings.ContainsRune(line, '5') { 79 | return errors.New("profile with existing proxy configuration") 80 | } 81 | } 82 | 83 | userjs := path.Join(profilePath, "user.js") 84 | if _, err = os.Stat(userjs); err == nil { 85 | if ok, _, _ := fileLineContains(userjs, userProfileHeader); !ok { 86 | return nil 87 | } 88 | } 89 | 90 | if err != nil && !errors.Is(err, fs.ErrNotExist) { 91 | return fmt.Errorf("failed checking whether file exists: %v", err) 92 | } 93 | 94 | return os.WriteFile(userjs, []byte(userPref), 0644) 95 | } 96 | 97 | func fileLineContains(file, substr string) (bool, string, error) { 98 | f, err := os.Open(file) 99 | if err != nil { 100 | return false, "", err 101 | } 102 | defer f.Close() 103 | 104 | sc := bufio.NewScanner(f) 105 | for sc.Scan() { 106 | line := sc.Text() 107 | if strings.Contains(line, substr) { 108 | return true, line, nil 109 | } 110 | } 111 | return false, "", nil 112 | } 113 | 114 | func getProfilePaths() ([]string, error) { 115 | c, err := os.UserConfigDir() 116 | if err != nil { 117 | return nil, fmt.Errorf("failed reading user config dir: %v", err) 118 | } 119 | 120 | pp := path.Join(c, relativeProfilesPath) 121 | dirs, err := os.ReadDir(pp) 122 | if err != nil { 123 | return nil, fmt.Errorf("failed listing profiles: %v", err) 124 | } 125 | 126 | var paths []string 127 | for _, dir := range dirs { 128 | if strings.Contains(dir.Name(), "default") { 129 | paths = append(paths, path.Join(pp, dir.Name())) 130 | } 131 | } 132 | 133 | return paths, nil 134 | } 135 | 136 | func readPEM(certPath string) ([]byte, error) { 137 | cert, err := ioutil.ReadFile(certPath) 138 | if err != nil { 139 | return nil, fmt.Errorf("failed reading root certificate: %v", err) 140 | } 141 | 142 | // Decode PEM 143 | certBlock, _ := pem.Decode(cert) 144 | if certBlock == nil || certBlock.Type != "CERTIFICATE" { 145 | return nil, fmt.Errorf("failed decoding cert invalid PEM data") 146 | } 147 | 148 | return certBlock.Bytes, nil 149 | } 150 | 151 | func readX509Cert(certPath string) (*x509.Certificate, error) { 152 | cert, err := readPEM(certPath) 153 | if err != nil { 154 | return nil, err 155 | } 156 | c, err := x509.ParseCertificate(cert) 157 | if err != nil { 158 | return nil, err 159 | } 160 | 161 | return c, nil 162 | } 163 | -------------------------------------------------------------------------------- /internal/config/auto/install_darwin.go: -------------------------------------------------------------------------------- 1 | //+build darwin 2 | 3 | package auto 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "regexp" 11 | "strings" 12 | ) 13 | 14 | var re = regexp.MustCompile(`(?m)^\([0-9]+\)(.+)`) 15 | var relativeProfilesPath = "Firefox/Profiles" 16 | 17 | func VerifyCert(certPath string) (err error) { 18 | // TODO: use Go API once supported 19 | // current api doesn't support reloading 20 | // https://github.com/golang/go/issues/46287 21 | _, err = runCommand("security", "verify-cert", "-c", certPath) 22 | return err 23 | } 24 | 25 | // Supported whether auto configuration 26 | // is supported for this build 27 | func Supported() bool { 28 | return true 29 | } 30 | 31 | func getProxyStatusForService(service, autoURL string) (ProxyStatus, error) { 32 | out, err := runCommand("networksetup", "-getautoproxyurl", service) 33 | if err != nil { 34 | return ProxyStatusConflict, err 35 | } 36 | 37 | url, enabled := parseGetAutoURL(string(out)) 38 | if !enabled { 39 | return ProxyStatusNone, nil 40 | } 41 | 42 | if url == "" { 43 | return ProxyStatusNone, nil 44 | } 45 | 46 | if !equalURL(url, autoURL) { 47 | return ProxyStatusConflict, nil 48 | } 49 | 50 | return ProxyStatusInstalled, nil 51 | } 52 | 53 | func UninstallAutoProxy(autoURL string) { 54 | services, err := getNetworkServices() 55 | if err != nil { 56 | return 57 | } 58 | 59 | for _, service := range services { 60 | status, err := getProxyStatusForService(service, autoURL) 61 | if err != nil { 62 | continue 63 | } 64 | 65 | if status == ProxyStatusInstalled { 66 | _, _ = runCommand("networksetup", "-setautoproxystate", service, "off") 67 | } 68 | } 69 | } 70 | 71 | func InstallAutoProxy(autoURL string) error { 72 | services, err := getNetworkServices() 73 | if err != nil { 74 | return fmt.Errorf("failed reading network services") 75 | } 76 | 77 | var lastErr error 78 | installed := 0 79 | 80 | for _, service := range services { 81 | status, err := getProxyStatusForService(service, autoURL) 82 | if err != nil { 83 | lastErr = fmt.Errorf("failed checking proxy status") 84 | continue 85 | } 86 | if status == ProxyStatusConflict { 87 | lastErr = fmt.Errorf("auto configuration failed your OS has existing proxy settings") 88 | } 89 | if status != ProxyStatusNone { 90 | continue 91 | } 92 | if _, err = runCommand("networksetup", "-setautoproxyurl", service, autoURL); err != nil { 93 | lastErr = fmt.Errorf("failed configuring proxy make sure your user account has permissions to change proxy settings") 94 | continue 95 | } 96 | if _, err = runCommand("networksetup", "-setautoproxystate", service, "on"); err != nil { 97 | lastErr = fmt.Errorf("failed changing proxy state for service %s", service) 98 | continue 99 | } 100 | 101 | installed++ 102 | } 103 | 104 | if installed > 0 { 105 | return nil 106 | } 107 | 108 | return lastErr 109 | } 110 | 111 | func runCommand(name string, args ...string) ([]byte, error) { 112 | cmd := exec.Command(name, args...) 113 | return cmd.CombinedOutput() 114 | } 115 | 116 | func getNetworkServices() ([]string, error) { 117 | cmd := exec.Command("networksetup", "-listnetworkserviceorder") 118 | out, err := cmd.CombinedOutput() 119 | if err != nil { 120 | return nil, fmt.Errorf("failed reading network services: %v", err) 121 | } 122 | 123 | var services []string 124 | for _, match := range re.FindAllStringSubmatch(string(out), -1) { 125 | if len(match) != 2 { 126 | continue 127 | } 128 | services = append(services, strings.TrimSpace(match[1])) 129 | } 130 | 131 | return services, nil 132 | } 133 | 134 | func InstallCert(certPath string) error { 135 | dir, err := os.UserHomeDir() 136 | if err != nil { 137 | return err 138 | } 139 | 140 | kp := path.Join(dir, "Library/Keychains/login.keychain") 141 | out, err := runCommand("security", "add-trusted-cert", 142 | "-p", "basic", "-p", "ssl", "-k", kp, certPath) 143 | 144 | if err == nil { 145 | return nil 146 | } 147 | 148 | if _, ok := err.(*exec.ExitError); ok { 149 | return fmt.Errorf("failed installing cert: %s", string(out)) 150 | } 151 | 152 | return fmt.Errorf("failed installing cert: %v", err) 153 | } 154 | 155 | func UninstallCert(certPath string) error { 156 | _, err := runCommand("security", "remove-trusted-cert", certPath) 157 | return err 158 | } 159 | 160 | // parses networksetup -getautoproxyurl 161 | func parseGetAutoURL(txt string) (url string, enabled bool) { 162 | lines := strings.Split(strings.TrimSpace(txt), "\n") 163 | for _, line := range lines { 164 | if strings.HasPrefix(line, "URL:") { 165 | url = strings.TrimSpace(line[4:]) 166 | continue 167 | } 168 | 169 | if strings.HasPrefix(line, "Enabled:") { 170 | line = strings.TrimSpace(line[8:]) 171 | enabled = line != "No" 172 | continue 173 | } 174 | } 175 | 176 | return 177 | } 178 | -------------------------------------------------------------------------------- /internal/config/pages/setup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fingertip - Manual Setup 5 | 123 | 124 | 125 |
126 |

Fingertip

127 | 137 |

1 Install Certificate

138 |

Your private CA is stored at {{.CertPath}}.

139 |

140 | It cannot be used to issue certificates for legacy domains (ending with .com, .net ... etc) since it uses the name constraints extension. Add this CA to your browser/TLS client trust store to 141 | allow Fingertip to issue certificates for decentralized names. 142 |

143 | 144 |
145 | Download Certificate 146 |
147 | 148 |

2 Configure proxy

149 |

Choose Automatic Proxy configuration in your browser/TLS client proxy settings and add this url:

150 |
{{.PACLink}}
151 | 152 |
153 | Fingertip v{{.Version}} 154 |
155 |
156 | 157 | 158 | -------------------------------------------------------------------------------- /internal/resolvers/proc/hns.go: -------------------------------------------------------------------------------- 1 | package proc 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os/exec" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | type HNSProc struct { 16 | path string 17 | args []string 18 | resolverAddr string 19 | rootAddr string 20 | cmd *exec.Cmd 21 | Verbose bool 22 | procStarted bool 23 | height uint64 24 | lastHeightUpdate time.Time 25 | synced bool 26 | retryCount int 27 | lastRetry time.Time 28 | sync.RWMutex 29 | } 30 | 31 | func NewHNSProc(procPath string, rootAddr, recursiveAddr string) (*HNSProc, error) { 32 | args := []string{"--ns-host", rootAddr, "--rs-host", recursiveAddr, "--pool-size", "4"} 33 | 34 | if !strings.HasSuffix(procPath, processExtension) { 35 | procPath += processExtension 36 | } 37 | 38 | p := &HNSProc{ 39 | path: procPath, 40 | args: args, 41 | resolverAddr: recursiveAddr, 42 | rootAddr: rootAddr, 43 | Verbose: true, 44 | } 45 | 46 | return p, nil 47 | } 48 | 49 | func (h *HNSProc) SetUserAgent(agent string) { 50 | extra := []string{"--user-agent", agent} 51 | h.args = append(h.args, extra...) 52 | } 53 | 54 | func (h *HNSProc) goStart(stopErr chan<- error) { 55 | go func() { 56 | h.cmd = exec.Command(h.path, h.args...) 57 | h.cmd.SysProcAttr = processAttributes 58 | 59 | pipe, err := h.cmd.StdoutPipe() 60 | if err != nil { 61 | log.Printf("[WARN] hns: couldn't read from process %v", err) 62 | return 63 | } 64 | h.cmd.Stderr = h.cmd.Stdout 65 | 66 | if err := h.cmd.Start(); err != nil { 67 | stopErr <- err 68 | return 69 | } 70 | 71 | h.monitor(pipe, stopErr) 72 | }() 73 | 74 | } 75 | 76 | func (h *HNSProc) monitor(pipe io.ReadCloser, stopErr chan<- error) { 77 | sc := bufio.NewScanner(pipe) 78 | p := "chain (" 79 | plen := len(p) 80 | for sc.Scan() { 81 | t := sc.Text() 82 | if h.Verbose { 83 | log.Printf("[INFO] hns: %s", t) 84 | } 85 | 86 | if !strings.HasPrefix(t, p) { 87 | continue 88 | } 89 | 90 | var block []rune 91 | for _, r := range t[plen:] { 92 | if r == ')' { 93 | break 94 | } 95 | block = append(block, r) 96 | } 97 | 98 | val, err := strconv.ParseUint(string(block), 10, 64) 99 | if err != nil { 100 | val = 0 101 | } 102 | 103 | h.SetHeight(val) 104 | // if we are getting some updates from hnsd process 105 | // it started successfully so we may want 106 | // to reset retry count 107 | h.maybeResetRetries() 108 | } 109 | 110 | if h.Verbose { 111 | log.Printf("[INFO] hns: closing process %v", sc.Err()) 112 | } 113 | 114 | if err := h.cmd.Wait(); err != nil { 115 | stopErr <- fmt.Errorf("process exited %v", err) 116 | return 117 | } 118 | 119 | stopErr <- fmt.Errorf("process exited 0") 120 | } 121 | 122 | func (h *HNSProc) killProcess() error { 123 | if h.cmd == nil || h.cmd.Process == nil { 124 | return nil 125 | } 126 | 127 | if err := h.cmd.Process.Kill(); err != nil { 128 | return err 129 | } 130 | 131 | return nil 132 | } 133 | 134 | func (h *HNSProc) Started() bool { 135 | h.RLock() 136 | defer h.RUnlock() 137 | 138 | return h.procStarted 139 | } 140 | 141 | func (h *HNSProc) SetStarted(s bool) { 142 | h.Lock() 143 | defer h.Unlock() 144 | 145 | h.procStarted = s 146 | } 147 | 148 | func (h *HNSProc) Retries() int { 149 | h.RLock() 150 | defer h.RUnlock() 151 | 152 | return h.retryCount 153 | } 154 | 155 | func (h *HNSProc) maybeResetRetries() { 156 | h.Lock() 157 | defer h.Unlock() 158 | 159 | if time.Since(h.lastRetry) > 10*time.Minute { 160 | h.retryCount = 0 161 | h.lastRetry = time.Time{} 162 | } 163 | } 164 | 165 | func (h *HNSProc) IncrementRetries() { 166 | h.Lock() 167 | defer h.Unlock() 168 | 169 | h.retryCount += 1 170 | h.lastRetry = time.Now() 171 | } 172 | 173 | func (h *HNSProc) SetHeight(height uint64) { 174 | h.Lock() 175 | defer h.Unlock() 176 | 177 | if h.height == height { 178 | return 179 | } 180 | 181 | h.height = height 182 | h.lastHeightUpdate = time.Now() 183 | } 184 | 185 | func (h *HNSProc) GetHeight() uint64 { 186 | h.RLock() 187 | defer h.RUnlock() 188 | 189 | return h.height 190 | } 191 | 192 | func (h *HNSProc) Synced() bool { 193 | h.RLock() 194 | defer h.RUnlock() 195 | 196 | if h.synced { 197 | return true 198 | } 199 | 200 | h.synced = !h.lastHeightUpdate.IsZero() && 201 | time.Since(h.lastHeightUpdate) > 20*time.Second 202 | 203 | return h.synced 204 | } 205 | 206 | func (h *HNSProc) Start(stopErr chan<- error) { 207 | if h.Started() { 208 | return 209 | } 210 | 211 | h.Lock() 212 | defer h.Unlock() 213 | 214 | h.goStart(stopErr) 215 | h.procStarted = true 216 | 217 | } 218 | 219 | func (h *HNSProc) Stop() { 220 | h.Lock() 221 | defer h.Unlock() 222 | h.killProcess() 223 | h.procStarted = false 224 | h.height = 0 225 | h.lastHeightUpdate = time.Time{} 226 | h.synced = false 227 | } 228 | -------------------------------------------------------------------------------- /internal/resolvers/util.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | // some utils adapted from 4 | // https://github.com/ethereum/go-ethereum/blob/release/1.7/contracts/ens/ens.go 5 | // https://github.com/wealdtech/go-ens/blob/904e0feb4c0df8478b11e9e475afde5852c87763/namehash.go 6 | 7 | import ( 8 | "fmt" 9 | "github.com/ethereum/go-ethereum/common" 10 | "github.com/ethereum/go-ethereum/crypto" 11 | "github.com/miekg/dns" 12 | "golang.org/x/crypto/sha3" 13 | "golang.org/x/net/idna" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | var p = idna.New(idna.MapForLookup(), idna.StrictDomainName(false), idna.Transitional(false)) 19 | 20 | func ensParentNode(name string) (common.Hash, common.Hash) { 21 | parts := strings.SplitN(name, ".", 2) 22 | label := crypto.Keccak256Hash([]byte(parts[0])) 23 | if len(parts) == 1 { 24 | return [32]byte{}, label 25 | } else { 26 | parentNode, parentLabel := ensParentNode(parts[1]) 27 | return crypto.Keccak256Hash(parentNode[:], parentLabel[:]), label 28 | } 29 | } 30 | 31 | func EnsNode(name string) common.Hash { 32 | parentNode, parentLabel := ensParentNode(name) 33 | return crypto.Keccak256Hash(parentNode[:], parentLabel[:]) 34 | } 35 | 36 | // Normalize normalizes a name according to the ENS rules 37 | func Normalize(input string) (output string, err error) { 38 | output, err = p.ToUnicode(input) 39 | if err != nil { 40 | return 41 | } 42 | // If the name started with a period then ToUnicode() removes it, but we want to keep it 43 | if strings.HasPrefix(input, ".") && !strings.HasPrefix(output, ".") { 44 | output = "." + output 45 | } 46 | return 47 | } 48 | 49 | // LabelHash generates a simple hash for a piece of a name. 50 | func LabelHash(label string) (hash [32]byte, err error) { 51 | normalizedLabel, err := Normalize(label) 52 | if err != nil { 53 | return 54 | } 55 | 56 | sha := sha3.NewLegacyKeccak256() 57 | if _, err = sha.Write([]byte(normalizedLabel)); err != nil { 58 | return 59 | } 60 | sha.Sum(hash[:0]) 61 | return 62 | } 63 | 64 | // NameHash generates a hash from a name that can be used to 65 | // look up the name in ENS 66 | func NameHash(name string) (hash [32]byte, err error) { 67 | if name == "" { 68 | return 69 | } 70 | normalizedName, err := Normalize(name) 71 | if err != nil { 72 | return 73 | } 74 | parts := strings.Split(normalizedName, ".") 75 | for i := len(parts) - 1; i >= 0; i-- { 76 | if hash, err = nameHashPart(hash, parts[i]); err != nil { 77 | return 78 | } 79 | } 80 | return 81 | } 82 | 83 | func nameHashPart(currentHash [32]byte, name string) (hash [32]byte, err error) { 84 | sha := sha3.NewLegacyKeccak256() 85 | if _, err = sha.Write(currentHash[:]); err != nil { 86 | return 87 | } 88 | 89 | nameSha := sha3.NewLegacyKeccak256() 90 | if _, err = nameSha.Write([]byte(name)); err != nil { 91 | return 92 | } 93 | nameHash := nameSha.Sum(nil) 94 | if _, err = sha.Write(nameHash); err != nil { 95 | return 96 | } 97 | sha.Sum(hash[:0]) 98 | return 99 | } 100 | 101 | func hashDnsName(name string) ([32]byte, error) { 102 | var qnameWire [266]byte 103 | off, err := dns.PackDomainName(name, qnameWire[:], 0, nil, false) 104 | if err != nil { 105 | return [32]byte{}, fmt.Errorf("error packing qname `%s`: %v", name, err) 106 | } 107 | 108 | var qnameHash [32]byte 109 | hash := crypto.Keccak256(qnameWire[:off]) 110 | copy(qnameHash[:], hash) 111 | 112 | return qnameHash, nil 113 | } 114 | 115 | func unpackRRSet(raw []byte) []dns.RR { 116 | if len(raw) == 0 { 117 | return nil 118 | } 119 | 120 | var ( 121 | rrs []dns.RR 122 | rr dns.RR 123 | err error 124 | rrOff = 0 125 | ) 126 | 127 | for { 128 | rr, rrOff, err = dns.UnpackRR(raw, rrOff) 129 | if err != nil || rr.Header().Rdlength == 0 { 130 | break 131 | } 132 | 133 | rrs = append(rrs, rr) 134 | if rrOff == len(raw) { 135 | break 136 | } 137 | } 138 | 139 | return rrs 140 | } 141 | 142 | func toNode(name string) string { 143 | return LastNLabels(name, 2) 144 | } 145 | 146 | // LastNLabels returns a lower cased string 147 | // with last n labels from the specified domain name 148 | func LastNLabels(name string, n int) string { 149 | name = dns.CanonicalName(name) 150 | parts := dns.SplitDomainName(name) 151 | if len(parts) <= n { 152 | return strings.Join(parts, ".") 153 | } 154 | 155 | return strings.Join(parts[len(parts)-n:], ".") 156 | } 157 | 158 | // FirstNLabels returns a lower cased string 159 | // with first n labels from the specified domain name 160 | func FirstNLabels(name string, n int) string { 161 | name = dns.CanonicalName(name) 162 | parts := dns.SplitDomainName(name) 163 | if len(parts) <= n { 164 | return strings.Join(parts, ".") 165 | } 166 | 167 | return strings.Join(parts[:n], ".") 168 | } 169 | 170 | func nsToRR(ns []*dns.NS) (rrs []dns.RR) { 171 | for _, rr := range ns { 172 | rrs = append(rrs, rr) 173 | } 174 | return 175 | } 176 | 177 | // getTTL finds the TTL of an RRSet 178 | // if records have different TTLs it will return the min 179 | // https://datatracker.ietf.org/doc/html/rfc2181#section-5 180 | func getTTL(rrs []dns.RR) time.Duration { 181 | var ttl uint32 = 10800 182 | for _, rr := range rrs { 183 | if ttl > rr.Header().Ttl { 184 | ttl = rr.Header().Ttl 185 | } 186 | } 187 | 188 | // min ttl 189 | if ttl < 60 { 190 | return time.Minute 191 | } 192 | 193 | // max ttl 194 | if ttl > 10800 { 195 | return time.Hour * 3 196 | } 197 | 198 | return time.Duration(ttl) * time.Second 199 | } 200 | -------------------------------------------------------------------------------- /internal/resolvers/ethereum.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/ethereum/go-ethereum/ethclient" 8 | "github.com/miekg/dns" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // hardcoded .eth NS rrset pointing to their registry 14 | var ethNS = []*dns.NS{ 15 | { 16 | Hdr: dns.RR_Header{ 17 | Name: "eth.", 18 | Rrtype: dns.TypeNS, 19 | Class: 1, 20 | Ttl: 86400, 21 | }, 22 | Ns: "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e._eth.", 23 | }, 24 | } 25 | 26 | type Ethereum struct { 27 | client *ethclient.Client 28 | // resolver cache 29 | rCache *cache 30 | // query cache 31 | qCache map[uint16]*cache 32 | } 33 | 34 | type queryCacheData struct { 35 | registry string 36 | rrs []dns.RR 37 | } 38 | 39 | func NewEthereum(rawurl string) (*Ethereum, error) { 40 | conn, err := ethclient.Dial(rawurl) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | e := &Ethereum{ 46 | client: conn, 47 | rCache: newCache(200), 48 | qCache: make(map[uint16]*cache), 49 | } 50 | 51 | // caching lower level lookups only 52 | // other types should be cached by users 53 | // of this client 54 | e.qCache[dns.TypeCNAME] = newCache(200) 55 | e.qCache[dns.TypeNS] = newCache(500) 56 | e.qCache[dns.TypeDS] = newCache(500) 57 | return e, nil 58 | } 59 | 60 | func (e *Ethereum) GetResolverAddress(node, registryAddress string) (common.Address, error) { 61 | key := node + ";" + registryAddress 62 | r, ok := e.rCache.get(key) 63 | if ok { 64 | if time.Now().Before(r.ttl) { 65 | return r.msg.(common.Address), nil 66 | } 67 | e.rCache.remove(key) 68 | } 69 | 70 | registry, err := NewENSRegistry(common.HexToAddress(registryAddress), e.client) 71 | if err != nil { 72 | return common.Address{}, err 73 | } 74 | 75 | normalizedName, err := Normalize(node) 76 | if err != nil { 77 | return common.Address{}, err 78 | } 79 | 80 | addr, err := registry.Resolver(nil, EnsNode(normalizedName)) 81 | if err != nil { 82 | return common.Address{}, err 83 | } 84 | 85 | e.rCache.set(key, &entry{ 86 | msg: addr, 87 | ttl: time.Now().Add(6 * time.Hour), 88 | }) 89 | 90 | return addr, nil 91 | } 92 | 93 | func isZero(addr common.Address) bool { 94 | for _, b := range addr { 95 | if b != 0 { 96 | return false 97 | } 98 | } 99 | 100 | return true 101 | } 102 | 103 | func (e *Ethereum) Resolve(registry string, ra common.Address, qname string, qtype uint16) ([]dns.RR, error) { 104 | if isZero(ra) { 105 | return nil, nil 106 | } 107 | 108 | r, err := NewDNSResolver(ra, e.client) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | qname = dns.CanonicalName(qname) 114 | node := toNode(qname) 115 | nodeHash, err := NameHash(node) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | res, err := e.queryWithResolver(registry, r, nodeHash, qname, qtype) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | return res, nil 126 | } 127 | 128 | func (e *Ethereum) checkQueryCache(registry string, qname string, qtype uint16) ([]dns.RR, bool) { 129 | c, ok := e.qCache[qtype] 130 | if !ok { 131 | return nil, false 132 | } 133 | entry, ok := c.get(qname) 134 | if !ok { 135 | return nil, false 136 | } 137 | 138 | if time.Now().After(entry.ttl) { 139 | c.remove(qname) 140 | return nil, false 141 | } 142 | 143 | m := entry.msg.(*queryCacheData) 144 | if !strings.EqualFold(m.registry, registry) { 145 | c.remove(qname) 146 | return nil, false 147 | } 148 | 149 | return m.rrs, true 150 | } 151 | 152 | func (e *Ethereum) dnsRecord(registry string, r *DNSResolver, node [32]byte, qname string, qtype uint16) ([]dns.RR, error) { 153 | if rrs, ok := e.checkQueryCache(registry, qname, qtype); ok { 154 | return rrs, nil 155 | } 156 | 157 | qnameHash, err := hashDnsName(qname) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | raw, err := r.DnsRecord(nil, node, qnameHash, qtype) 163 | if err != nil { 164 | return nil, err 165 | } 166 | 167 | rrs := unpackRRSet(raw) 168 | 169 | if qtype == dns.TypeCNAME || qtype == dns.TypeNS || qtype == dns.TypeDS { 170 | e.qCache[qtype].set(qname, &entry{ 171 | msg: &queryCacheData{ 172 | registry: registry, 173 | rrs: rrs, 174 | }, 175 | ttl: time.Now().Add(getTTL(rrs)), 176 | }) 177 | } 178 | 179 | return rrs, nil 180 | } 181 | 182 | func (e *Ethereum) queryWithResolver(registry string, r *DNSResolver, nodeHash [32]byte, qname string, qtype uint16) ([]dns.RR, error) { 183 | rawRecords, err := e.dnsRecord(registry, r, nodeHash, qname, qtype) 184 | if err != nil { 185 | return nil, err 186 | } 187 | 188 | maxLabels := dns.CountLabel(qname) 189 | if maxLabels > 3 { 190 | maxLabels = 3 191 | } 192 | 193 | // Look for NS records up to maxLabels 194 | if len(rawRecords) == 0 { 195 | labels := 2 196 | for { 197 | if labels > maxLabels { 198 | break 199 | } 200 | 201 | name := dns.Fqdn(LastNLabels(qname, labels)) 202 | labels++ 203 | 204 | if rawRecords, err = e.dnsRecord(registry, r, nodeHash, name, dns.TypeNS); err != nil { 205 | return nil, err 206 | } 207 | 208 | // a delegation exists check if it's signed 209 | if len(rawRecords) > 0 { 210 | var dsSet []dns.RR 211 | if dsSet, err = e.dnsRecord(registry, r, nodeHash, name, dns.TypeDS); err != nil { 212 | return nil, err 213 | } 214 | 215 | if len(dsSet) > 0 { 216 | rawRecords = append(rawRecords, dsSet...) 217 | } 218 | 219 | return rawRecords, nil 220 | } 221 | } 222 | } 223 | 224 | if len(rawRecords) == 0 { 225 | // no records for original qname and no delegations 226 | // check if a CNAME exists 227 | if rawRecords, err = e.dnsRecord(registry, r, nodeHash, qname, dns.TypeCNAME); err != nil { 228 | return nil, err 229 | } 230 | } 231 | 232 | return rawRecords, nil 233 | } 234 | 235 | func (e *Ethereum) Handler(ctx context.Context, qname string, qtype uint16, ns *dns.NS) ([]dns.RR, error) { 236 | registryAddress := FirstNLabels(ns.Ns, 1) 237 | node := toNode(qname) 238 | 239 | var resolverAddr common.Address 240 | var err error 241 | 242 | resolverAddr, err = e.GetResolverAddress(node, registryAddress) 243 | if err != nil { 244 | return nil, fmt.Errorf("unable to get resolver address from registry %s: %v", registryAddress, err) 245 | } 246 | 247 | return e.Resolve(registryAddress, resolverAddr, qname, qtype) 248 | } 249 | -------------------------------------------------------------------------------- /internal/resolvers/hip5_test.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/buffrr/letsdane/resolver" 8 | "github.com/miekg/dns" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestNewHIP5Resolver(t *testing.T) { 14 | dummyResolver := resolver.DefaultResolver{ 15 | Query: func(ctx context.Context, name string, qtype uint16) *resolver.DNSResult { 16 | return &resolver.DNSResult{ 17 | Records: nil, 18 | Secure: false, 19 | Err: resolver.ErrServFail, 20 | } 21 | }, 22 | } 23 | 24 | synced := false 25 | stub := &resolver.Stub{DefaultResolver: dummyResolver} 26 | 27 | h := NewHIP5Resolver(stub, "0.0.0.0", func() bool { 28 | return synced 29 | }) 30 | 31 | h.exchangeRoot = testExchangeRootFunc(t, "com.", 32 | []dns.RR{testRR("forever. 300 IN NS root-extension._example.")}) 33 | 34 | var errNoPancakes = errors.New("couldn't make pancakes") 35 | h.RegisterHandler("_example", func(ctx context.Context, qname string, qtype uint16, ns *dns.NS) ([]dns.RR, error) { 36 | return nil, errNoPancakes 37 | }) 38 | 39 | // has no hip-5 extension but shouldn't resolve until synced 40 | _, _, err := h.LookupIP(context.Background(), "ip", "example.net") 41 | if !errors.Is(err, errNotSynced) { 42 | t.Fatalf("got err = %v, want %v", err, errNotSynced) 43 | } 44 | 45 | // chain synced 46 | synced = true 47 | 48 | _, _, err = h.LookupIP(context.Background(), "ip", "example.com") 49 | if !errors.Is(err, errNoPancakes) { 50 | t.Fatalf("got err = %v, want %v", err, errNoPancakes) 51 | } 52 | } 53 | 54 | func testRR(str string) dns.RR { 55 | rr, err := dns.NewRR(str) 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | return rr 61 | } 62 | 63 | type rootFunc func(ctx context.Context, m *dns.Msg, a string) (r *dns.Msg, rtt time.Duration, err error) 64 | 65 | func testExchangeRootFunc(t *testing.T, tld string, nsRRs []dns.RR) rootFunc { 66 | return func(ctx context.Context, m *dns.Msg, a string) (r *dns.Msg, rtt time.Duration, err error) { 67 | m.Rcode = dns.RcodeSuccess 68 | if m.Question[0].Name != tld { 69 | t.Fatalf("got tld = %s, want %s", m.Question[0].Name, tld) 70 | } 71 | 72 | m.Ns = nsRRs 73 | return m, 0, nil 74 | } 75 | } 76 | 77 | func TestHIP5CNames(t *testing.T) { 78 | recursive := map[string]*resolver.DNSResult{ 79 | "example.com.": { 80 | Records: []dns.RR{testRR("example.com. 80000 IN A 93.184.216.34")}, 81 | }, 82 | "secure.test.": { 83 | Records: []dns.RR{testRR("secure.test. 80000 IN A 93.184.216.34")}, 84 | Secure: true, 85 | }, 86 | "blog.secure.test.": { 87 | Records: []dns.RR{ 88 | testRR("_443._tcp.secure.test. 80000 IN A 1.1.1.1"), 89 | testRR("_443._tcp.secure.test. 80000 IN A 1.0.0.1"), 90 | }, 91 | Secure: true, 92 | }, 93 | } 94 | 95 | hip5Data := map[string][]dns.RR{ 96 | "secure.forever.": {testRR("secure.forever. 300 IN CNAME secure.test.")}, 97 | "blog.secure.forever.": {testRR("blog.secure.forever. 300 IN CNAME blog.secure.test.")}, 98 | "loop.forever.": {testRR("loop.forever. 300 IN CNAME loop.forever.")}, 99 | "hello.forever.": {testRR("hello.forever. 300 IN CNAME example.com.")}, 100 | "hello2.forever.": {testRR("hello2.forever. 300 IN CNAME hello.forever.")}, 101 | "hello3.forever.": {testRR("hello3.forever. 300 IN CNAME hello2.forever.")}, 102 | "redirect.forever.": {testRR("redirect.forever. 300 IN CNAME secure.forever.")}, 103 | "redirect-insecure.forever.": {testRR("redirect-insecure.forever. 300 IN CNAME hello.forever.")}, 104 | "indirect-loop.forever.": {testRR("indirect-loop.forever. 300 IN CNAME loop.forever.")}, 105 | } 106 | 107 | tests := map[string]*resolver.DNSResult{ 108 | "secure.forever.": { 109 | Records: recursive["secure.test."].Records, 110 | Secure: true, 111 | }, 112 | "blog.secure.forever.": { 113 | Records: recursive["blog.secure.test."].Records, 114 | Secure: true, 115 | }, 116 | "loop.forever.": { 117 | Records: []dns.RR{}, 118 | Err: errBadCNAMETarget, 119 | }, 120 | "hello.forever.": { 121 | Records: recursive["example.com."].Records, 122 | }, 123 | "redirect.forever.": { 124 | Records: recursive["secure.test."].Records, 125 | Secure: true, 126 | }, 127 | "redirect-insecure.forever.": { 128 | Records: recursive["example.com."].Records, 129 | }, 130 | "indirect-loop.forever.": { 131 | Err: errBadCNAMETarget, 132 | }, 133 | "hello3.forever.": { 134 | Records: recursive["example.com."].Records, 135 | }, 136 | "hello12.forever.": { 137 | Err: errMaxDepthReached, 138 | }, 139 | } 140 | 141 | // long CNAME chain 142 | for i := 4; i < 13; i++ { 143 | name := fmt.Sprintf("hello%d.forever.", i) 144 | prev := fmt.Sprintf("hello%d.forever.", i-1) 145 | hip5Data[name] = []dns.RR{testRR(name + " 300 IN CNAME " + prev)} 146 | } 147 | 148 | dummyResolver := resolver.DefaultResolver{ 149 | Query: func(ctx context.Context, name string, qtype uint16) *resolver.DNSResult { 150 | if res, ok := recursive[name]; ok { 151 | return res 152 | } 153 | 154 | return &resolver.DNSResult{ 155 | Records: nil, 156 | Secure: false, 157 | Err: resolver.ErrServFail, 158 | } 159 | }, 160 | } 161 | 162 | stub := &resolver.Stub{DefaultResolver: dummyResolver} 163 | 164 | h := NewHIP5Resolver(stub, "0.0.0.0", func() bool { 165 | return true 166 | }) 167 | 168 | h.exchangeRoot = testExchangeRootFunc(t, "forever.", 169 | []dns.RR{testRR("forever. 300 IN NS bQHW1R4+11NRs0iWlCxlwyZZ1BxFVXqkNt+gszVTVl0=._example.")}) 170 | 171 | h.RegisterHandler("_example", func(ctx context.Context, qname string, qtype uint16, ns *dns.NS) ([]dns.RR, error) { 172 | if ans, ok := hip5Data[qname]; ok { 173 | return ans, nil 174 | } 175 | 176 | return nil, fmt.Errorf("unexpected qname = %s", qname) 177 | }) 178 | 179 | for name, test := range tests { 180 | t.Run(name, func(t *testing.T) { 181 | ips, secure, err := h.LookupIP(context.Background(), "ip4", name) 182 | if err != test.Err { 183 | if err == nil || test.Err == nil || !errors.Is(err, test.Err) { 184 | t.Fatalf("got err = %v, want = %v", err, test.Err) 185 | } 186 | } 187 | 188 | if secure != test.Secure { 189 | t.Fatalf("got secure = %v, want = %v", secure, test.Secure) 190 | } 191 | 192 | if len(ips) != len(test.Records) { 193 | t.Fatalf("got results len = %d, want %d", len(ips), len(test.Records)) 194 | } 195 | }) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /internal/config/debug.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "fingertip/internal/resolvers" 6 | "fmt" 7 | "github.com/buffrr/letsdane/resolver" 8 | "github.com/miekg/dns" 9 | "math/rand" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | const ( 16 | letterIdxBits = 6 17 | letterIdxMask = 1< 0: 86 | return fmt.Errorf("your network appears to intercept and redirect outgoing DNS requests") 87 | } 88 | 89 | referral := false 90 | hasRRSIGs := false 91 | 92 | for _, rr := range r.Ns { 93 | if rr.Header().Rrtype == dns.TypeNS && strings.EqualFold("isc.org.", rr.Header().Name) { 94 | referral = true 95 | } 96 | if rr.Header().Rrtype == dns.TypeRRSIG { 97 | hasRRSIGs = true 98 | } 99 | } 100 | 101 | if referral { 102 | if hasRRSIGs { 103 | return nil 104 | } 105 | return fmt.Errorf("received a response without DNSSEC signatures") 106 | } 107 | 108 | return fmt.Errorf("received unexpected referral") 109 | } 110 | 111 | func exchangeWithRetry(m *dns.Msg, addrs []string) (r *dns.Msg, rtt time.Duration, err error) { 112 | serverId := 0 113 | for i := 0; i < 3; i++ { 114 | if r, rtt, err = dnsTestClient.Exchange(m, addrs[serverId]); err == nil { 115 | if !r.Truncated { 116 | return 117 | } 118 | } 119 | serverId = (serverId + 1) % len(addrs) 120 | } 121 | 122 | return 123 | } 124 | 125 | func (d *Debugger) SetBlockHeight(h uint64) { 126 | d.Lock() 127 | defer d.Unlock() 128 | 129 | d.blockHeight = h 130 | } 131 | 132 | func (d *Debugger) Ping() { 133 | d.Lock() 134 | defer d.Unlock() 135 | 136 | d.lastPing = time.Now() 137 | } 138 | 139 | func (d *Debugger) GetLastPing() time.Time { 140 | d.RLock() 141 | defer d.RUnlock() 142 | 143 | return d.lastPing 144 | } 145 | 146 | func (d *Debugger) SetCheckCert(c func() bool) { 147 | d.Lock() 148 | defer d.Unlock() 149 | 150 | d.checkCert = c 151 | } 152 | 153 | func (d *Debugger) SetCheckSynced(s func() bool) { 154 | d.Lock() 155 | defer d.Unlock() 156 | 157 | d.checkSynced = s 158 | } 159 | 160 | func (d *Debugger) NewProbe() { 161 | d.Lock() 162 | d.proxyProbeReached = false 163 | d.proxyProbeDomain = randString(50) 164 | d.dnsProbeInProgress = true 165 | d.Unlock() 166 | 167 | go func() { 168 | err := testDNSInterference() 169 | d.Lock() 170 | d.dnsProbeInProgress = false 171 | d.dnsProbeErr = err 172 | d.Unlock() 173 | }() 174 | } 175 | 176 | func (d *Debugger) GetInfo() DebugInfo { 177 | d.RLock() 178 | defer d.RUnlock() 179 | 180 | var err string 181 | if d.dnsProbeErr != nil { 182 | err = d.dnsProbeErr.Error() 183 | } 184 | return DebugInfo{ 185 | BlockHeight: d.blockHeight, 186 | ProbeURL: "http://" + d.proxyProbeDomain, 187 | ProbeReached: d.proxyProbeReached, 188 | Syncing: d.checkSynced != nil && !d.checkSynced(), 189 | CertInstalled: d.checkCert != nil && d.checkCert(), 190 | DNSReachable: !d.dnsProbeInProgress && d.dnsProbeErr == nil, 191 | DNSProbeErr: err, 192 | DNSProbeInProgress: d.dnsProbeInProgress, 193 | } 194 | } 195 | 196 | func (d *Debugger) GetDNSProbeMiddleware() resolvers.QueryMiddlewareFunc { 197 | return func(qname string, qtype uint16) (bool, *resolver.DNSResult) { 198 | d.RLock() 199 | probeName := d.proxyProbeDomain 200 | skipName := d.proxyProbeReached || len(probeName) != len(qname) 201 | d.RUnlock() 202 | 203 | if skipName { 204 | return false, nil 205 | } 206 | 207 | if strings.EqualFold(probeName, qname) { 208 | d.Lock() 209 | d.proxyProbeReached = true 210 | d.Unlock() 211 | 212 | return true, &resolver.DNSResult{ 213 | Records: nil, 214 | Secure: false, 215 | Err: errors.New(""), 216 | } 217 | } 218 | 219 | return false, nil 220 | } 221 | } 222 | 223 | // source: http://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang 224 | func randString(n int) string { 225 | sb := strings.Builder{} 226 | sb.Grow(n) 227 | 228 | for i, cache, remain := n-1, weakRandSrc.Int63(), letterIdxMax; i >= 0; { 229 | if remain == 0 { 230 | cache, remain = weakRandSrc.Int63(), letterIdxMax 231 | } 232 | if idx := int(cache & letterIdxMask); idx < len(letterBytes) { 233 | sb.WriteByte(letterBytes[idx]) 234 | i-- 235 | } 236 | cache >>= letterIdxBits 237 | remain-- 238 | } 239 | 240 | return sb.String() 241 | } 242 | -------------------------------------------------------------------------------- /internal/resolvers/dnssec/testdata/val_huque.com.txt: -------------------------------------------------------------------------------- 1 | [ZONE] origin: busted.huque.com., time: 20210824000000 2 | [TRUST_ANCHORS] 3 | busted.huque.com. 86400 IN DS 35992 8 2 51CE90A0BABD30DD9A40AFB217C7BD43A70405E58C0D0C9217508D05 84E506B0 4 | 5 | [DNSKEYS] min_rsa_keysize: 1024 6 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 60559 7 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1 8 | 9 | ;; OPT PSEUDOSECTION: 10 | ; EDNS: version: 0, flags: do; udp: 1232 11 | ;; QUESTION SECTION: 12 | ;busted.huque.com. IN DNSKEY 13 | 14 | ;; ANSWER SECTION: 15 | busted.huque.com. 229 IN DNSKEY 256 3 8 AwEAAcwABEdDh7dFnAi2ixWHpnhuTAMZZ8zd+1KjQSRvbZ5IEitLcacy 0bAzcL2lHoCcFBPSb/DoMR+Fo+RO0B6KZK8yznOQg9vu7czKoZNIEj/E C7j5N01YIXzpwsHHM5Sadhr6nl0i572PKrmRRgN8iaKNSizQz5ikUB3B Sfn+G+tN 16 | busted.huque.com. 229 IN DNSKEY 257 3 8 AwEAAdoovHGXXaqD5dt62KW/cwFxPnogBonlLNiHI2F27XSXgwxaU3/n SkKQbwIa6/9DqCELz79zXQnnKzSADjOYHcSVEzFcYG1lH1+lgUPTg2oF emjSQvB9agLALwBvXeh66iRI/7Cj4ALJl0mitUhxcfH0MajieRzy2DAk wGydjK+zYO7OfhMp7oxGbPoMSQptwDxioQ/Zg435l8HziF6IcPp+wbKX hIj7JjxqgruCfVMZUdkLPI6xDV7K+m0h83b0j5gtNiuFVRNliFfFSSbc PleyojmopPT+naaY6uSHkoiEVFiKIin86Dk5keRVenfOL5cnGHNYYL0U o4wDC6u1zps= 17 | busted.huque.com. 229 IN RRSIG DNSKEY 8 3 7200 20210922133002 20210724123002 35992 busted.huque.com. Ak2G4jnLM3bSkKxzGQFT1eZgU2DL6lw9JzRmC6Qdx3XEqxhOUbzmyrAQ EmpP29F1lBJ95l5rCssoPdjjZkUyJGknU++6SFlb/dzo5xKxcU4vW10P cPXa3eghppQspi6Zj3SIdbJfsmhYnxESaXklFUr4H/VeBf896HwzMHG1 o7EsJbbTdvuPWz8KMnU7/CEnGVXvWRmHHlDmkPHxDz7Ac0YSBsAw59KU HxQ7vxq/UL6k5GrM8SrwVBYt9IAXFRfXWJOByu06tgj4t+/6r/+7zV4/ LaSxv/VEBH/NqfTSpZgiPFZSurTgN6s6Md0kYZg2TUcFYKQ2iNd0gFuD ePsL0Q== 18 | 19 | 20 | [TEST_BEGIN] name: bad signature 21 | [INPUT] 22 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 62738 23 | ;; flags: qr rd ra cd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 24 | 25 | ;; OPT PSEUDOSECTION: 26 | ; EDNS: version: 0, flags: do; udp: 512 27 | ;; QUESTION SECTION: 28 | ;_443._tcp.badsig.busted.huque.com. IN TLSA 29 | 30 | ;; ANSWER SECTION: 31 | _443._tcp.badsig.busted.huque.com. 7199 IN TLSA 3 1 1 62E50C202F3A971DCC1CE977E993734D6088C29F89D37703F8F512C5 43B8D371 32 | _443._tcp.badsig.busted.huque.com. 7199 IN RRSIG TLSA 8 6 7200 20210922133002 20210724123002 7101 busted.huque.com. mHwuiIqCxcm7Hcm4zftj9s1rqlDkiLU5B52y2ibuzAFzF3JVw6x57tGX oXdbhKwJL8jWbVUsy3a1OHlSGVj0iawVvFIrQjWr4vTh5ht6W2Y+lu+2 m0Q/ht5XOSuxUdcoZWfLacsfrk7qZrTsuxqt6f0RiY9lyQTPf2N3C5tS tDc= 33 | 34 | [RESULT] secure: 0, bogus: 1 35 | [TEST_END] 36 | 37 | 38 | [TEST_BEGIN] name: expired signature 39 | [INPUT] 40 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 6770 41 | ;; flags: qr rd ra cd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 42 | 43 | ;; OPT PSEUDOSECTION: 44 | ; EDNS: version: 0, flags: do; udp: 512 45 | ;; QUESTION SECTION: 46 | ;_443._tcp.expiredsig.busted.huque.com. IN TLSA 47 | 48 | ;; ANSWER SECTION: 49 | _443._tcp.expiredsig.busted.huque.com. 7199 IN TLSA 3 1 1 62E50C202F3A971DCC1CE977E993734D6088C29F89D37703F8F512C5 43B8D371 50 | _443._tcp.expiredsig.busted.huque.com. 7199 IN RRSIG TLSA 8 6 7200 20170225091500 20170125091500 7101 busted.huque.com. QKTzqlLxZMLbMc749ZZqWtFsZhipynVTi4xvSvjD8anGkGMcNDd36ZhI sjSbF7hefCTBVdHmHbb+Jroh18meBX3sogYQucjXfZ+rjGZTyunr40P7 GWG1bh8IGGP0C3O9axzayxL7RhERBNT/gMFlue30F9rq/Xe55SRVWR9D Ndk= 51 | 52 | [RESULT] secure: 0, bogus: 1 53 | [TEST_END] 54 | 55 | 56 | [TEST_BEGIN] name: downgrade nsec3 57 | [INPUT] 58 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 28519 59 | ;; flags: qr rd ra cd; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1 60 | 61 | ;; OPT PSEUDOSECTION: 62 | ; EDNS: version: 0, flags: do; udp: 512 63 | ;; QUESTION SECTION: 64 | ;badsig.busted.huque.com. IN AAAA 65 | 66 | ;; AUTHORITY SECTION: 67 | busted.huque.com. 1799 IN SOA mname.huque.com. hostmaster.huque.com. 1000000298 43200 3600 3628800 3600 68 | busted.huque.com. 3599 IN RRSIG SOA 8 3 7200 20210922133002 20210724123002 7101 busted.huque.com. p6T+ermQRSCKS9EomJdfMhVTduEr5Wn51XVRpVSkkVJP+9AOcILYsC+7 wYxNN0zYaH8mRfK2UCXzHNfKxTCVkmpikF89cAt58lJgPkWronF94RqA wfV4w6vERq9L7rNzURQj9+MJGSh0p8sjjNkvECyPoMhZCziY6hUZRcZa Edw= 69 | 0UDODQILMC9TNL71U4SHERDG7AI4HJCL.busted.huque.com. 3599 IN NSEC3 1 0 5 A7B2182A738FCBC4 2BRFH7T5ANI8UV9643QI6SUGF6PRLCM2 A RRSIG 70 | 0UDODQILMC9TNL71U4SHERDG7AI4HJCL.busted.huque.com. 3599 IN RRSIG NSEC3 8 4 3600 20210922133002 20210724123002 7101 busted.huque.com. xywCY//3T4dUKruVhD3zG3YDdsa8BVrOuC7Hoe4LZBfjFNVb2pJk1j9F l2QRjoIz4+TXLyd9x+L65RqPKZQpuclhiUSjUzfHgHilolBz4uUXB8Hf jyJhC237zGtC9aGf4J+5WhDlLJ5v3YKBX6kUt8fS9jIs1RnhvM23eHhm ftM= 71 | 72 | [RESULT] secure: 0, bogus: 0 73 | [TEST_END] 74 | 75 | 76 | [TEST_BEGIN] name: downgrade nsec3 remove answer section 77 | [INPUT] 78 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 6770 79 | ;; flags: qr rd ra cd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 80 | 81 | ;; OPT PSEUDOSECTION: 82 | ; EDNS: version: 0, flags: do; udp: 512 83 | ;; QUESTION SECTION: 84 | ;_443._tcp.expiredsig.busted.huque.com. IN TLSA 85 | 86 | ;; ANSWER SECTION: 87 | _443._tcp.expiredsig.busted.huque.com. 7199 IN TLSA 3 1 1 62E50C202F3A971DCC1CE977E993734D6088C29F89D37703F8F512C5 43B8D371 88 | _443._tcp.expiredsig.busted.huque.com. 7199 IN RRSIG TLSA 8 6 7200 20170225091500 20170125091500 7101 busted.huque.com. QKTzqlLxZMLbMc749ZZqWtFsZhipynVTi4xvSvjD8anGkGMcNDd36ZhI sjSbF7hefCTBVdHmHbb+Jroh18meBX3sogYQucjXfZ+rjGZTyunr40P7 GWG1bh8IGGP0C3O9axzayxL7RhERBNT/gMFlue30F9rq/Xe55SRVWR9D Ndk= 89 | 90 | ;; AUTHORITY SECTION: 91 | HJGCSRCC2VLMTQSN4VRMRLU0G1MGD0PV.busted.huque.com. 3599 IN NSEC3 1 0 5 A7B2182A738FCBC4 0UDODQILMC9TNL71U4SHERDG7AI4HJCL RRSIG TLSA 92 | HJGCSRCC2VLMTQSN4VRMRLU0G1MGD0PV.busted.huque.com. 3599 IN RRSIG NSEC3 8 4 3600 20210922133002 20210724123002 7101 busted.huque.com. bWNM4ED6YRGNBxPDfz/r39oBw0+ZzKZsClmXVkAONzfQ/5W0e1hKFHEB QEGmOEs9L9VERDK4g6oOyDxOS1A2tnJlSJVOS2S9Bcn/8lVnV7P2K6/7 veHytkOfHZVP1AfoidvcN5THJJH+DQS9LF4uB2sV0UcjxjB2sRU1vArB 4ew= 93 | 94 | [RESULT] secure: 0, bogus: 0 95 | [VERIFY_MESSAGE] 96 | ; invalid answer section should be removed from filtered response 97 | ;; AUTHORITY SECTION: 98 | HJGCSRCC2VLMTQSN4VRMRLU0G1MGD0PV.busted.huque.com. 3599 IN NSEC3 1 0 5 A7B2182A738FCBC4 0UDODQILMC9TNL71U4SHERDG7AI4HJCL RRSIG TLSA 99 | HJGCSRCC2VLMTQSN4VRMRLU0G1MGD0PV.busted.huque.com. 3599 IN RRSIG NSEC3 8 4 3600 20210922133002 20210724123002 7101 busted.huque.com. bWNM4ED6YRGNBxPDfz/r39oBw0+ZzKZsClmXVkAONzfQ/5W0e1hKFHEB QEGmOEs9L9VERDK4g6oOyDxOS1A2tnJlSJVOS2S9Bcn/8lVnV7P2K6/7 veHytkOfHZVP1AfoidvcN5THJJH+DQS9LF4uB2sV0UcjxjB2sRU1vArB 4ew= 100 | 101 | [TEST_END] 102 | -------------------------------------------------------------------------------- /internal/resolvers/dnssec/testdata/val_cloudflare.com.txt: -------------------------------------------------------------------------------- 1 | [ZONE] origin: cloudflare.com., time: 20210824000000 2 | [TRUST_ANCHORS] 3 | cloudflare.com. 80167 IN DS 2371 13 2 32996839A6D808AFE3EB4A795A0E6A7A39A76FC52FF228B22B76F6D6 3826F2B9 4 | 5 | [DNSKEYS] 6 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 27557 7 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1 8 | 9 | ;; OPT PSEUDOSECTION: 10 | ; EDNS: version: 0, flags: do; udp: 1232 11 | ;; QUESTION SECTION: 12 | ;cloudflare.com. IN DNSKEY 13 | 14 | ;; ANSWER SECTION: 15 | cloudflare.com. 1099 IN DNSKEY 256 3 13 oJMRESz5E4gYzS/q6XDrvU1qMPYIjCWzJaOau8XNEZeqCYKD5ar0IRd8 KqXXFJkqmVfRvMGPmM1x8fGAa2XhSA== 16 | cloudflare.com. 1099 IN DNSKEY 257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+ KkxLbxILfDLUT0rAK9iUzy1L53eKGQ== 17 | cloudflare.com. 1099 IN RRSIG DNSKEY 13 2 3600 20210911041030 20210713041030 2371 cloudflare.com. VC6Zb7vRKcjpkGHqJZ0Sn+gjbf1GXNhrCbcZCZl7+A0Ib9KdCWSXI2Hw m2L4XAAEjEp0tEk+nJzaQiIPCYm/fQ== 18 | 19 | 20 | [TEST_BEGIN] name: valid nodata proof with online signing 21 | [INPUT] 22 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 26693 23 | ;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1 24 | ;; WARNING: recursion requested but not available 25 | 26 | ;; OPT PSEUDOSECTION: 27 | ; EDNS: version: 0, flags: do; udp: 1232 28 | ;; QUESTION SECTION: 29 | ;cloudflare.com. IN TLSA 30 | 31 | ;; AUTHORITY SECTION: 32 | cloudflare.com. 300 IN SOA ns3.cloudflare.com. dns.cloudflare.com. 2038144483 10000 2400 604800 300 33 | cloudflare.com. 300 IN NSEC \000.cloudflare.com. A NS SOA HINFO MX TXT AAAA LOC SRV NAPTR CERT SSHFP RRSIG NSEC DNSKEY SMIMEA HIP CDS CDNSKEY OPENPGPKEY TYPE64 TYPE65 SPF URI CAA 34 | cloudflare.com. 300 IN RRSIG SOA 13 2 300 20210825093916 20210823073916 34505 cloudflare.com. nL1Q/9zB2N7mpaktW91nZYJrdQ2Fg+R+ki7G47DyLip66mf3t7BGVKlW IUjXwIRX9SW2Z0B0w6NyJAVoWYHZOQ== 35 | cloudflare.com. 300 IN RRSIG NSEC 13 2 300 20210825093916 20210823073916 34505 cloudflare.com. ug4JG+94Klq+9RVoAdGNDAzXFr+wh11Pgisa2jRcwLgPPQKiHb+E/x0f Ev5v9Y6Tt9NH3zs2Z1svy87CV1aX9A== 36 | [RESULT] secure: 1, bogus: 0 37 | [TEST_END] 38 | 39 | [TEST_BEGIN] name: expired nodata proof 40 | [INPUT] 41 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 35712 42 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1 43 | 44 | ;; OPT PSEUDOSECTION: 45 | ; EDNS: version: 0, flags: do; udp: 1232 46 | ;; QUESTION SECTION: 47 | ;cloudflare.com. IN TLSA 48 | 49 | ;; AUTHORITY SECTION: 50 | cloudflare.com. 300 IN SOA ns3.cloudflare.com. dns.cloudflare.com. 2038096097 10000 2400 604800 300 51 | cloudflare.com. 300 IN RRSIG SOA 13 2 300 20210821072939 20210819052939 34505 cloudflare.com. qOvF4ZmrxbyvqAUSvxToXbKFFohSDsufTyaVX8zqydO8iAwfIRzAUW5k cSSmXOSRFEjW4KiBNaKZui3nJcYKrQ== 52 | cloudflare.com. 300 IN NSEC \000.cloudflare.com. A NS SOA HINFO MX TXT AAAA LOC SRV NAPTR CERT SSHFP RRSIG NSEC DNSKEY SMIMEA HIP CDS CDNSKEY OPENPGPKEY TYPE64 TYPE65 SPF URI CAA 53 | cloudflare.com. 300 IN RRSIG NSEC 13 2 300 20210821072939 20210819052939 34505 cloudflare.com. fWGieKjUeWGxfNLDCMDg8R4WmxWi2cgwm4YPr41Vj530aDmWipk2ls1g ca71BHVg9HMeD4x6wtzeajjcCWQ+Xw== 54 | [RESULT] secure: 0, bogus: 1 55 | [TEST_END] 56 | 57 | [TEST_BEGIN] name: invalid rrsigs 58 | [INPUT] 59 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 35712 60 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1 61 | 62 | ;; OPT PSEUDOSECTION: 63 | ; EDNS: version: 0, flags: do; udp: 1232 64 | ;; QUESTION SECTION: 65 | ;cloudflare.com. IN TLSA 66 | 67 | ;; AUTHORITY SECTION: 68 | cloudflare.com. 300 IN NSEC \000.cloudflare.com. A NS SOA HINFO MX TXT AAAA LOC SRV NAPTR CERT SSHFP RRSIG NSEC DNSKEY SMIMEA HIP CDS CDNSKEY OPENPGPKEY TYPE64 TYPE65 SPF URI CAA 69 | cloudflare.com. 300 IN RRSIG NSEC 12 2 300 20210821072939 20210819052939 34505 cloudflare.com. fWGieKjUeWGxfNLDCMDg8R4WmxWi2cgwm4YPr41Vj530aDmWipk2ls1g ca71BHVg9HMeD4x6wtzeajjcCWQ+Xw== 70 | [RESULT] secure: 0, bogus: 1 71 | [TEST_END] 72 | 73 | 74 | [TEST_BEGIN] name: answer doesn't match question 75 | [INPUT] 76 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 35712 77 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1 78 | 79 | ;; OPT PSEUDOSECTION: 80 | ; EDNS: version: 0, flags: do; udp: 1232 81 | ;; QUESTION SECTION: 82 | ;cloudflare.com. IN TLSA 83 | 84 | ;; ANSWER SECTION: 85 | cloudflare.com. 281 IN RRSIG A 13 2 300 20210821074310 20210819054310 34505 cloudflare.com. qxSYQaAiBiAdbUOdjn1C6WveTg2W/0+yOV/3cVxoMlYGp0UKe4uqxhAU 4HyJqZ7ats2qLlFtlc29DnJcOfgh+g== 86 | cloudflare.com. 281 IN A 104.16.133.229 87 | cloudflare.com. 281 IN A 104.16.132.229 88 | [RESULT] secure: 0, bogus: 1 89 | [TEST_END] 90 | 91 | 92 | [TEST_BEGIN] name: sanitize answer 93 | [INPUT] 94 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 35712 95 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1 96 | 97 | ;; OPT PSEUDOSECTION: 98 | ; EDNS: version: 0, flags: do; udp: 1232 99 | ;; QUESTION SECTION: 100 | ;cloudflare.com. IN A 101 | 102 | ;; ANSWER SECTION: 103 | cloudflare.com. 300 IN RRSIG A 13 2 300 20210825094047 20210823074047 34505 cloudflare.com. DKTDYwE1Zg8xCyc6tUVu6oCIw1a4BLan9mS+yiM7JssvpaFyZtoGSJQ/ GNS+F0GIO79lIyNeQ5y5gYKhPltxpw== 104 | cloudflare.com. 71 IN A 104.16.132.229 105 | cloudflare.com. 71 IN A 104.16.133.229 106 | blog.cloudflare.com. 300 IN A 104.18.26.46 107 | blog.cloudflare.com. 300 IN A 104.18.27.46 108 | blog.cloudflare.com. 300 IN RRSIG A 13 3 300 20210821074537 20210819054537 34505 blog.cloudflare.com. c8Bv9HG/n0VY7Upu9FE6b0yyUBwuChpxl65ASAV4hhLEQsjnf0nrlsYB mbX+SjVdjNAxMKlu7aUCxUKl3lOgOA== 109 | [RESULT] secure: 1, bogus: 0 110 | ; blog.cloudflare.com. shouldn't be included otherwise its good 111 | [VERIFY_MESSAGE] 112 | ;; ANSWER SECTION: 113 | cloudflare.com. 300 IN RRSIG A 13 2 300 20210825094047 20210823074047 34505 cloudflare.com. DKTDYwE1Zg8xCyc6tUVu6oCIw1a4BLan9mS+yiM7JssvpaFyZtoGSJQ/ GNS+F0GIO79lIyNeQ5y5gYKhPltxpw== 114 | cloudflare.com. 71 IN A 104.16.132.229 115 | cloudflare.com. 71 IN A 104.16.133.229 116 | [TEST_END] -------------------------------------------------------------------------------- /internal/config/auto/install_windows.go: -------------------------------------------------------------------------------- 1 | //+build windows 2 | 3 | package auto 4 | 5 | // crypto API usage based on https://github.com/FiloSottile/mkcert/blob/master/truststore_windows.go 6 | 7 | import ( 8 | "crypto/x509" 9 | "fmt" 10 | "golang.org/x/sys/windows/registry" 11 | "math/big" 12 | "syscall" 13 | "unsafe" 14 | ) 15 | 16 | var relativeProfilesPath = "Mozilla/Firefox/Profiles" 17 | 18 | func VerifyCert(certPath string) (err error) { 19 | var cert *x509.Certificate 20 | if cert, err = readX509Cert(certPath); err != nil { 21 | return 22 | } 23 | 24 | _, err = cert.Verify(x509.VerifyOptions{}) 25 | return 26 | } 27 | 28 | var ( 29 | modcrypt32 = syscall.NewLazyDLL("crypt32.dll") 30 | procCertAddEncodedCertificateToStore = modcrypt32.NewProc("CertAddEncodedCertificateToStore") 31 | procCertCloseStore = modcrypt32.NewProc("CertCloseStore") 32 | procCertDeleteCertificateFromStore = modcrypt32.NewProc("CertDeleteCertificateFromStore") 33 | procCertDuplicateCertificateContext = modcrypt32.NewProc("CertDuplicateCertificateContext") 34 | procCertEnumCertificatesInStore = modcrypt32.NewProc("CertEnumCertificatesInStore") 35 | procCertOpenSystemStoreW = modcrypt32.NewProc("CertOpenSystemStoreW") 36 | ) 37 | 38 | type windowsRootStore uintptr 39 | 40 | // Supported whether auto configuration 41 | // is supported for this build 42 | func Supported() bool { 43 | return true 44 | } 45 | 46 | func getProxyStatus(autoURL string) (ProxyStatus, error) { 47 | k, err := registry.OpenKey(registry.CURRENT_USER, 48 | `SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings`, registry.QUERY_VALUE) 49 | if err != nil { 50 | return ProxyStatusNone, err 51 | } 52 | defer k.Close() 53 | 54 | value, _, _ := k.GetStringValue("AutoConfigURL") 55 | if equalURL(autoURL, value) { 56 | return ProxyStatusInstalled, nil 57 | } 58 | 59 | if value == "" { 60 | return ProxyStatusNone, nil 61 | } 62 | 63 | return ProxyStatusConflict, nil 64 | } 65 | 66 | func UninstallAutoProxy(autoURL string) { 67 | if status, err := getProxyStatus(autoURL); err != nil || status == ProxyStatusConflict { 68 | return 69 | } 70 | 71 | k, err := registry.OpenKey(registry.CURRENT_USER, 72 | `SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings`, registry.ALL_ACCESS) 73 | if err != nil { 74 | return 75 | } 76 | defer k.Close() 77 | 78 | _ = k.DeleteValue("AutoConfigURL") 79 | return 80 | } 81 | 82 | func InstallAutoProxy(autoURL string) error { 83 | status, err := getProxyStatus(autoURL) 84 | if err != nil { 85 | return fmt.Errorf("failed reading proxy status") 86 | } 87 | if status == ProxyStatusConflict { 88 | return fmt.Errorf("auto configuration failed your OS has existing proxy settings") 89 | } 90 | if status == ProxyStatusInstalled { 91 | return nil 92 | } 93 | 94 | k, err := registry.OpenKey(registry.CURRENT_USER, 95 | `SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings`, registry.ALL_ACCESS) 96 | if err != nil { 97 | return fmt.Errorf("failed reading from registry: %v", err) 98 | } 99 | defer k.Close() 100 | 101 | if err = k.SetStringValue("AutoConfigURL", autoURL); err != nil { 102 | return fmt.Errorf("failed setting AutoConfigURL: %v", err) 103 | } 104 | return nil 105 | } 106 | 107 | func InstallCert(certPath string) error { 108 | // Load cert 109 | cert, err := readPEM(certPath) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | // Open root store 115 | store, err := openWindowsRootStore() 116 | if err != nil { 117 | return err 118 | } 119 | defer store.close() 120 | // Add cert 121 | if err := store.addCert(cert); err != nil { 122 | return err 123 | } 124 | 125 | return nil 126 | } 127 | 128 | func UninstallCert(certPath string) error { 129 | cert, err := readX509Cert(certPath) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | // We'll just remove all certs with the same serial number 135 | // Open root store 136 | store, err := openWindowsRootStore() 137 | if err != nil { 138 | return err 139 | } 140 | defer store.close() 141 | 142 | // Do the deletion 143 | deletedAny, err := store.deleteCertsWithSerial(cert.SerialNumber) 144 | if err == nil && !deletedAny { 145 | err = fmt.Errorf("no certs found") 146 | } 147 | 148 | if err != nil { 149 | return fmt.Errorf("failed deleting cert: %v", err) 150 | } 151 | 152 | return nil 153 | } 154 | 155 | func openWindowsRootStore() (windowsRootStore, error) { 156 | rootStr, err := syscall.UTF16PtrFromString("ROOT") 157 | if err != nil { 158 | return 0, err 159 | } 160 | store, _, err := procCertOpenSystemStoreW.Call(0, uintptr(unsafe.Pointer(rootStr))) 161 | if store != 0 { 162 | return windowsRootStore(store), nil 163 | } 164 | return 0, fmt.Errorf("failed opening windows root store: %v", err) 165 | } 166 | 167 | func (w windowsRootStore) close() error { 168 | ret, _, err := procCertCloseStore.Call(uintptr(w), 0) 169 | if ret != 0 { 170 | return nil 171 | } 172 | return fmt.Errorf("failed closing windows root store: %v", err) 173 | } 174 | 175 | func (w windowsRootStore) addCert(cert []byte) error { 176 | ret, _, err := procCertAddEncodedCertificateToStore.Call( 177 | uintptr(w), // HCERTSTORE hCertStore 178 | uintptr(syscall.X509_ASN_ENCODING|syscall.PKCS_7_ASN_ENCODING), // DWORD dwCertEncodingType 179 | uintptr(unsafe.Pointer(&cert[0])), // const BYTE *pbCertEncoded 180 | uintptr(len(cert)), // DWORD cbCertEncoded 181 | 3, // DWORD dwAddDisposition (CERT_STORE_ADD_REPLACE_EXISTING is 3) 182 | 0, // PCCERT_CONTEXT *ppCertContext 183 | ) 184 | if ret != 0 { 185 | return nil 186 | } 187 | return fmt.Errorf("failed adding cert: %v", err) 188 | } 189 | 190 | func (w windowsRootStore) deleteCertsWithSerial(serial *big.Int) (bool, error) { 191 | // Go over each, deleting the ones we find 192 | var cert *syscall.CertContext 193 | deletedAny := false 194 | for { 195 | // Next enum 196 | certPtr, _, err := procCertEnumCertificatesInStore.Call(uintptr(w), uintptr(unsafe.Pointer(cert))) 197 | if cert = (*syscall.CertContext)(unsafe.Pointer(certPtr)); cert == nil { 198 | if errno, ok := err.(syscall.Errno); ok && errno == 0x80092004 { 199 | break 200 | } 201 | return deletedAny, fmt.Errorf("failed enumerating certs: %v", err) 202 | } 203 | // Parse cert 204 | certBytes := (*[1 << 20]byte)(unsafe.Pointer(cert.EncodedCert))[:cert.Length] 205 | parsedCert, err := x509.ParseCertificate(certBytes) 206 | // We'll just ignore parse failures for now 207 | if err == nil && parsedCert.SerialNumber != nil && parsedCert.SerialNumber.Cmp(serial) == 0 { 208 | // Duplicate the context so it doesn't stop the enum when we delete it 209 | dupCertPtr, _, err := procCertDuplicateCertificateContext.Call(uintptr(unsafe.Pointer(cert))) 210 | if dupCertPtr == 0 { 211 | return deletedAny, fmt.Errorf("failed duplicating context: %v", err) 212 | } 213 | if ret, _, err := procCertDeleteCertificateFromStore.Call(dupCertPtr); ret == 0 { 214 | return deletedAny, fmt.Errorf("failed deleting certificate: %v", err) 215 | } 216 | deletedAny = true 217 | } 218 | } 219 | return deletedAny, nil 220 | } 221 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/json" 8 | "encoding/pem" 9 | "fmt" 10 | "github.com/buffrr/letsdane" 11 | "io/ioutil" 12 | "net/http" 13 | "os" 14 | "path" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | const AppName = "Fingertip" 20 | const AppId = "com.impervious.fingertip" 21 | const CertFileName = "fingertip.crt" 22 | const CertKeyFileName = "private.key" 23 | const CertName = "DNSSEC" 24 | 25 | type App struct { 26 | Path string 27 | CertPath string 28 | keyPath string 29 | DNSProcPath string 30 | Proxy letsdane.Config 31 | ProxyAddr string 32 | Version string 33 | 34 | Store *Store 35 | Debug Debugger 36 | } 37 | 38 | func getOrCreateDir() (string, error) { 39 | home, err := os.UserConfigDir() 40 | if err != nil { 41 | return "", err 42 | } 43 | 44 | p := path.Join(home, AppName) 45 | if _, err := os.Stat(p); err != nil { 46 | if err := os.Mkdir(p, 0700); err != nil { 47 | return "", err 48 | } 49 | } 50 | 51 | return p, nil 52 | } 53 | 54 | func (c *App) getOrCreateCA() (string, string, error) { 55 | certPath := path.Join(c.Path, CertFileName) 56 | keyPath := path.Join(c.Path, CertKeyFileName) 57 | 58 | if _, err := os.Stat(certPath); err != nil { 59 | if _, err := os.Stat(keyPath); err != nil { 60 | ca, priv, err := letsdane.NewAuthority(CertName, CertName, 365*24*time.Hour, nameConstraints) 61 | if err != nil { 62 | return "", "", fmt.Errorf("couldn't generate CA: %v", err) 63 | } 64 | 65 | certOut, err := os.OpenFile(certPath, os.O_CREATE|os.O_WRONLY, 0644) 66 | if err != nil { 67 | return "", "", fmt.Errorf("couldn't create CA file: %v", err) 68 | } 69 | defer certOut.Close() 70 | 71 | pem.Encode(certOut, &pem.Block{ 72 | Type: "CERTIFICATE", 73 | Bytes: ca.Raw, 74 | }) 75 | 76 | privOut := bytes.NewBuffer([]byte{}) 77 | pem.Encode(privOut, &pem.Block{ 78 | Type: "RSA PRIVATE KEY", 79 | Bytes: x509.MarshalPKCS1PrivateKey(priv), 80 | }) 81 | 82 | kOut, err := os.OpenFile(keyPath, os.O_CREATE|os.O_WRONLY, 0600) 83 | if err != nil { 84 | return "", "", fmt.Errorf("couldn't create CA private key file: %v", err) 85 | } 86 | defer kOut.Close() 87 | 88 | kOut.Write(privOut.Bytes()) 89 | return certPath, keyPath, nil 90 | } 91 | } 92 | return certPath, keyPath, nil 93 | } 94 | 95 | func loadX509KeyPair(certFile, keyFile string) (tls.Certificate, error) { 96 | certPEMBlock, err := ioutil.ReadFile(certFile) 97 | if err != nil { 98 | return tls.Certificate{}, err 99 | } 100 | 101 | keyPEMBlock, err := ioutil.ReadFile(keyFile) 102 | if err != nil { 103 | return tls.Certificate{}, err 104 | } 105 | 106 | return tls.X509KeyPair(certPEMBlock, keyPEMBlock) 107 | } 108 | 109 | func (c *App) loadCA() (*x509.Certificate, interface{}, error) { 110 | var x509c *x509.Certificate 111 | var priv interface{} 112 | var err error 113 | 114 | c.CertPath, c.keyPath, err = c.getOrCreateCA() 115 | if err != nil { 116 | return nil, nil, err 117 | } 118 | 119 | var cert tls.Certificate 120 | if cert, err = loadX509KeyPair(c.CertPath, c.keyPath); err != nil { 121 | return nil, nil, err 122 | } 123 | 124 | priv = cert.PrivateKey 125 | if x509c, err = x509.ParseCertificate(cert.Certificate[0]); err != nil { 126 | return nil, nil, err 127 | } 128 | 129 | return x509c, priv, nil 130 | } 131 | 132 | func getPACScript(proxyAddr string, names []string) string { 133 | skippedNames := fmt.Sprintf(`'%s'`, strings.Join(names, `', '`)) 134 | 135 | pac := fmt.Sprintf(` 136 | function FindProxyForURL(url, host) { 137 | var skipped = [ %s ]; 138 | 139 | // skip any TLD in the list 140 | var tld = host; 141 | var lastDot = tld.lastIndexOf('.'); 142 | if (lastDot != -1) { 143 | tld = tld.substr(lastDot+1); 144 | } 145 | tld = tld.toLowerCase(); 146 | 147 | if (skipped.includes(tld)) { 148 | return 'DIRECT'; 149 | } 150 | 151 | // skip IP addresses 152 | var isIpV4Addr = /^(\d+.){3}\d+$/; 153 | if (isIpV4Addr.test(host)) { 154 | return "DIRECT"; 155 | } 156 | 157 | // loosely check if IPv6 158 | if (lastDot == -1 && host.split(':').length > 2) { 159 | return "DIRECT"; 160 | } 161 | 162 | return "PROXY %s"; 163 | } 164 | `, skippedNames, proxyAddr) 165 | 166 | return pac 167 | } 168 | 169 | func NewConfig() (*App, error) { 170 | var err error 171 | c := &App{} 172 | if c.Path, err = getOrCreateDir(); err != nil { 173 | return nil, fmt.Errorf("failed creating config: %v", err) 174 | } 175 | 176 | c.Proxy.Constraints = nameConstraints 177 | c.Proxy.SkipNameChecks = false 178 | c.Proxy.Verbose = false 179 | c.Proxy.Validity = time.Hour 180 | c.Proxy.ContentHandler = &contentHandler{c} 181 | if c.Proxy.Certificate, c.Proxy.PrivateKey, err = c.loadCA(); err != nil { 182 | return nil, fmt.Errorf("failed creating config: %v", err) 183 | } 184 | 185 | c.Debug.NewProbe() 186 | c.Store, _ = readStore(path.Join(c.Path, "init"), c.Version, nil) 187 | 188 | return c, nil 189 | } 190 | 191 | type contentHandler struct { 192 | config *App 193 | } 194 | 195 | func (c *contentHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 196 | if req.URL.Path == "" || req.URL.Path == "/" { 197 | url := GetProxyURL(c.config.ProxyAddr) 198 | statusTmpl.Execute(rw, onBoardingTmplData{ 199 | CertPath: c.config.CertPath, 200 | CertLink: url + "/" + CertFileName, 201 | PACLink: url + "/proxy.pac", 202 | Version: c.config.Version, 203 | NavSetupLink: url + "/setup", 204 | NavStatusLink: url, 205 | }) 206 | return 207 | } 208 | 209 | if req.URL.Path == "/setup" { 210 | url := GetProxyURL(c.config.ProxyAddr) 211 | setupTmpl.Execute(rw, onBoardingTmplData{ 212 | CertPath: c.config.CertPath, 213 | CertLink: url + "/" + CertFileName, 214 | PACLink: url + "/proxy.pac", 215 | Version: c.config.Version, 216 | NavSetupLink: url + "/setup", 217 | NavStatusLink: url, 218 | }) 219 | return 220 | } 221 | 222 | if req.URL.Path == "/"+CertFileName { 223 | rw.Header().Set("Content-Type", "application/x-x509-ca-cert") 224 | rw.Write(pem.EncodeToMemory(&pem.Block{ 225 | Type: "CERTIFICATE", 226 | Bytes: c.config.Proxy.Certificate.Raw, 227 | })) 228 | 229 | return 230 | } 231 | 232 | if req.URL.Path == "/info.json" { 233 | if req.URL.Query().Get("init") == "1" { 234 | c.config.Debug.NewProbe() 235 | } 236 | 237 | rw.Header().Set("Content-Type", "application/json") 238 | data, _ := json.Marshal(c.config.Debug.GetInfo()) 239 | rw.Write(data) 240 | c.config.Debug.Ping() 241 | return 242 | } 243 | 244 | if req.URL.Path == "/proxy.pac" { 245 | rw.Header().Set("Content-Type", "application/x-ns-proxy-autoconfig") 246 | var names []string 247 | for n := range nameConstraints { 248 | names = append(names, n) 249 | } 250 | fmt.Fprint(rw, getPACScript(c.config.ProxyAddr, names)) 251 | return 252 | } 253 | } 254 | 255 | func GetProxyURL(addr string) string { 256 | parts := strings.SplitN(addr, ":", 2) 257 | if len(parts) < 2 { 258 | return addr 259 | } 260 | 261 | if parts[0] == "" { 262 | parts[0] = "127.0.0.1" 263 | } 264 | 265 | return "http://" + strings.Join(parts, ":") 266 | } 267 | 268 | func init() { 269 | // Skip reserved names in RFC2606 and special use TLDs such as .local 270 | // https://datatracker.ietf.org/doc/html/rfc2606 271 | var testNames = []string{"localhost", "test", "invalid", "example", "local"} 272 | for _, name := range testNames { 273 | nameConstraints[name] = struct{}{} 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /internal/resolvers/dnssec/testdata/val_example.com.txt: -------------------------------------------------------------------------------- 1 | [ZONE] origin: example.com., time: 20210824000000 2 | [TRUST_ANCHORS] 3 | example.com. 81925 IN DS 31406 8 1 189968811E6EBA862DD6C209F75623D8D9ED9142 4 | example.com. 81925 IN DS 31406 8 2 F78CF3344F72137235098ECBBD08947C2C9001C7F6A085A17F518B5D 8F6B916D 5 | example.com. 81925 IN DS 31589 8 1 3490A6806D47F17A34C29E2CE80E8A999FFBE4BE 6 | example.com. 81925 IN DS 31589 8 2 CDE0D742D6998AA554A92D890F8184C698CFAC8A26FA59875A990C03 E576343C 7 | example.com. 81925 IN DS 43547 8 1 B6225AB2CC613E0DCA7962BDC2342EA4F1B56083 8 | example.com. 81925 IN DS 43547 8 2 615A64233543F66F44D68933625B17497C89A70E858ED76A2145997E DF96A918 9 | 10 | ; default minimum allowed rsa key size is 2048 11 | [DNSKEYS] min_rsa_keysize: 1024 12 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 14064 13 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 1 14 | 15 | ;; OPT PSEUDOSECTION: 16 | ; EDNS: version: 0, flags: do; udp: 1232 17 | ;; QUESTION SECTION: 18 | ;example.com. IN DNSKEY 19 | 20 | ;; ANSWER SECTION: 21 | example.com. 1076 IN DNSKEY 256 3 8 AwEAAd0r756MOcFM1jtDwNY/45mvMBIvpnxz7X7pIZ/KzhFuBQ8n7Wlo KUCvlrlF6hljlsO0dXDJUvY9N1Q+kjWGTVQjXRHwEngIfU8cVwOraYoM bIcp9ty0hSXqgijNu7sVVRrWfhsfyFI82AFMjXpoKwyaMUe8/VT4OUkl E5gdYXAR 22 | example.com. 1076 IN DNSKEY 257 3 8 AwEAAZ0aqu1rJ6orJynrRfNpPmayJZoAx9Ic2/Rl9VQWLMHyjxxem3VU SoNUIFXERQbj0A9Ogp0zDM9YIccKLRd6LmWiDCt7UJQxVdD+heb5Ec4q lqGmyX9MDabkvX2NvMwsUecbYBq8oXeTT9LRmCUt9KUt/WOi6DKECxoG /bWTykrXyBR8elD+SQY43OAVjlWrVltHxgp4/rhBCvRbmdflunaPIgu2 7eE2U4myDSLT8a4A0rB5uHG4PkOa9dIRs9y00M2mWf4lyPee7vi5few2 dbayHXmieGcaAHrx76NGAABeY393xjlmDNcUkF1gpNWUla4fWZbbaYQz A93mLdrng+M= 23 | example.com. 1076 IN DNSKEY 257 3 8 AwEAAbOFAxl+Lkt0UMglZizKEC1AxUu8zlj65KYatR5wBWMrh18TYzK/ ig6Y1t5YTWCO68bynorpNu9fqNFALX7bVl9/gybA0v0EhF+dgXmoUfRX 7ksMGgBvtfa2/Y9a3klXNLqkTszIQ4PEMVCjtryl19Be9/PkFeC9ITjg MRQsQhmB39eyMYnal+f3bUxKk4fq7cuEU0dbRpue4H/N6jPucXWOwiMA kTJhghqgy+o9FfIp+tR/emKao94/wpVXDcPf5B18j7xz2SvTTxiuqCzC MtsxnikZHcoh1j4g+Y1B8zIMIvrEM+pZGhh/Yuf4RwCBgaYCi9hpiMWV vS4WBzx0/lU= 24 | example.com. 1076 IN RRSIG DNSKEY 8 2 3600 20210909083317 20210819162603 31406 example.com. PQf0v25B4Y9bTEViRrGSm09/6jeiaF0rn5nQwA8XwggK8liNYXhz0P8m WD+SrvvkDXmnRC1Ef/VHqwz7mNcoJA2OAnMstoyi8l2nj2z5mBUtOt1O btbC1lENSBpNxb/1Va0KznjrXgODleD1UrVYGAThNExY6Sa2T32NfUiQ wSp7XZwY2uDb1SUeMdaQ+FwM8NKwEHKp8j+qM4jr8MFY2kgKDKH6aYxz Ym2WdNmZO9RhZ+sNZSjP0mNyB4JlK9XzdpU10EBuDjRe56gIApjIKqmn zvxgGzy/pMAeq2Fy6+xgo8qBGBDwNjbsJjoKweYf0YK2Ej6ztMjCJHes riGnug== 25 | example.com. 1076 IN RRSIG DNSKEY 8 2 3600 20210909083317 20210819162603 45620 example.com. lwjyOQfmvG0cj3BMAn/nzXwqRxbMIt1xeofF+Iti1sHCULiQonIS7f7U F7FfYK4QQp+9zwpCMLNbmOL8MTSRsEDxh9rwabAlZkkTURC3jq7Olem/ 4C9eDZtDoChkzfUEWmZsX4XpQ1sdg1A4a0V6/zIL1OO58mWyXcs5mIHv HSfgAtx/WEHzSBlPOoGhJQGaQq5lFqoOqwqjHHwtYvyPqEy0qe2Njc07 S0bap8Pqaelraf5TqOuo1A5SjXuGDOgw6GR81JKIf5tR8lR4ISZ54MVM QgCggRPOIjg2cG5CGwq7507Jt5Sa5ddf8kaYjcyN/ty4dwO3buclWs3h mbIyBg== 26 | 27 | [TEST_BEGIN] name: verify nxdomain 28 | [INPUT] 29 | ;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 52196 30 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1 31 | ;; QUESTION SECTION: 32 | ;blog.example.com. IN A 33 | 34 | ;; AUTHORITY SECTION: 35 | example.com. 3600 IN SOA ns.icann.org. noc.dns.icann.org. 2021072012 7200 3600 1209600 3600 36 | example.com. 3600 IN RRSIG SOA 8 2 3600 20210826145054 20210805042559 21664 example.com. KDST+50+SII2aG3XxjMwzxAXQ4+FbNwXWfuJ+NIxb1Egh2Z3U6XfpL5v bieTTmOU7fq9nEANRbhff2GvS6byEFg8GcLwspaQRLJR68s7UjFGqk1f 8eVH1OOQ43+sg2S5/u33N7z2/Kr1fx/hr8QR61NA8kSxrsouqvP/DcWS R3o= 37 | example.com. 3600 IN NSEC www.example.com. A NS SOA MX TXT AAAA RRSIG NSEC DNSKEY 38 | example.com. 3600 IN RRSIG NSEC 8 2 3600 20210826152150 20210805042559 21664 example.com. 022GOnm7vHzpvZkJx2ZiwcgP2R11I9pCTHPtbnloYHi3uH6vVIXrVVQy ClDoFizUmxWPZGNjgOSdz4jODcE4lEpYVypn6XyMp2PoAQPnm0MFtH3J 8X1vWmAZnRS8xCyq2LR37j23dDLDvf2Pqrw8LKL6AlMu/Q1GrC++Ikrn 734= 39 | [RESULT] secure: 1, bogus: 0 40 | [TEST_END] 41 | 42 | 43 | [TEST_BEGIN] name: verify nodata 44 | [INPUT] 45 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 38761 46 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1 47 | ;; QUESTION SECTION: 48 | ;example.com. IN TLSA 49 | 50 | ;; AUTHORITY SECTION: 51 | example.com. 3600 IN SOA ns.icann.org. noc.dns.icann.org. 2021072012 7200 3600 1209600 3600 52 | example.com. 3600 IN RRSIG SOA 8 2 3600 20210826145054 20210805042559 21664 example.com. KDST+50+SII2aG3XxjMwzxAXQ4+FbNwXWfuJ+NIxb1Egh2Z3U6XfpL5v bieTTmOU7fq9nEANRbhff2GvS6byEFg8GcLwspaQRLJR68s7UjFGqk1f 8eVH1OOQ43+sg2S5/u33N7z2/Kr1fx/hr8QR61NA8kSxrsouqvP/DcWS R3o= 53 | example.com. 3600 IN NSEC www.example.com. A NS SOA MX TXT AAAA RRSIG NSEC DNSKEY 54 | example.com. 3600 IN RRSIG NSEC 8 2 3600 20210826152150 20210805042559 21664 example.com. 022GOnm7vHzpvZkJx2ZiwcgP2R11I9pCTHPtbnloYHi3uH6vVIXrVVQy ClDoFizUmxWPZGNjgOSdz4jODcE4lEpYVypn6XyMp2PoAQPnm0MFtH3J 8X1vWmAZnRS8xCyq2LR37j23dDLDvf2Pqrw8LKL6AlMu/Q1GrC++Ikrn 734= 55 | [RESULT] secure: 1, bogus: 0 56 | [TEST_END] 57 | 58 | 59 | [TEST_BEGIN] name: verify & sanitize answer 60 | [INPUT] 61 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 49139 62 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1 63 | 64 | ;; OPT PSEUDOSECTION: 65 | ; EDNS: version: 0, flags: do; udp: 1232 66 | ;; QUESTION SECTION: 67 | ;example.com. IN TXT 68 | 69 | ;; ANSWER SECTION: 70 | example.com. 86400 IN TXT "v=spf1 -all" 71 | example.com. 86400 IN TXT "8j5nfqld20zpcyr8xjw0ydcfq9rk8hgm" 72 | example.com. 86400 IN RRSIG TXT 8 2 86400 20210825180653 20210804222559 21664 example.com. IEwx+IIGHHBJuugE6l20SSupXfhalYqvF7LflxLK4BNCKnIvr26yIk5o CPT8In/POWu+JZEcmoTrPykq+bVCSWq7LfISlNQSmlFMvi+SiAa2lLU5 rib1+FP86y9F8AzPshCgSg9rtN39xAOtdpobfaE4TkAP/PUvhNWLVw0J Qjk= 73 | example.org. 7197 IN A 93.184.216.34 74 | [RESULT] secure: 1, bogus: 0 75 | [VERIFY_MESSAGE] 76 | ;; ANSWER SECTION: 77 | example.com. 86400 IN TXT "v=spf1 -all" 78 | example.com. 86400 IN TXT "8j5nfqld20zpcyr8xjw0ydcfq9rk8hgm" 79 | example.com. 86400 IN RRSIG TXT 8 2 86400 20210825180653 20210804222559 21664 example.com. IEwx+IIGHHBJuugE6l20SSupXfhalYqvF7LflxLK4BNCKnIvr26yIk5o CPT8In/POWu+JZEcmoTrPykq+bVCSWq7LfISlNQSmlFMvi+SiAa2lLU5 rib1+FP86y9F8AzPshCgSg9rtN39xAOtdpobfaE4TkAP/PUvhNWLVw0J Qjk= 80 | [TEST_END] 81 | 82 | [TEST_BEGIN] name: invalid nodata proof type exists in bitmap 83 | [INPUT] 84 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 49139 85 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1 86 | 87 | ;; OPT PSEUDOSECTION: 88 | ; EDNS: version: 0, flags: do; udp: 1232 89 | ;; QUESTION SECTION: 90 | ;example.com. IN TXT 91 | 92 | ;; AUTHORITY SECTION: 93 | example.com. 86400 IN NS ns1.example.org. 94 | example.com. 3600 IN NSEC www.example.com. A NS SOA MX TXT AAAA RRSIG NSEC DNSKEY 95 | example.com. 3600 IN RRSIG NSEC 8 2 3600 20210826152150 20210805042559 21664 example.com. 022GOnm7vHzpvZkJx2ZiwcgP2R11I9pCTHPtbnloYHi3uH6vVIXrVVQy ClDoFizUmxWPZGNjgOSdz4jODcE4lEpYVypn6XyMp2PoAQPnm0MFtH3J 8X1vWmAZnRS8xCyq2LR37j23dDLDvf2Pqrw8LKL6AlMu/Q1GrC++Ikrn 734= 96 | [RESULT] secure: 0, bogus: 1 97 | [TEST_END] 98 | 99 | [TEST_BEGIN] name: self delegation 100 | [INPUT] 101 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 49139 102 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1 103 | 104 | ;; OPT PSEUDOSECTION: 105 | ; EDNS: version: 0, flags: do; udp: 1232 106 | ;; QUESTION SECTION: 107 | ;example.com. IN TLSA 108 | 109 | ;; AUTHORITY SECTION: 110 | example.com. 86400 IN NS ns1.example.org. 111 | example.com. 3600 IN NSEC www.example.com. A NS SOA MX TXT AAAA RRSIG NSEC DNSKEY 112 | example.com. 3600 IN RRSIG NSEC 8 2 3600 20210826152150 20210805042559 21664 example.com. 022GOnm7vHzpvZkJx2ZiwcgP2R11I9pCTHPtbnloYHi3uH6vVIXrVVQy ClDoFizUmxWPZGNjgOSdz4jODcE4lEpYVypn6XyMp2PoAQPnm0MFtH3J 8X1vWmAZnRS8xCyq2LR37j23dDLDvf2Pqrw8LKL6AlMu/Q1GrC++Ikrn 734= 113 | [RESULT] secure: 1, bogus: 0 114 | [VERIFY_MESSAGE] 115 | ; the bad NS record must be removed otherwise the proof is good 116 | ;; AUTHORITY SECTION: 117 | example.com. 3600 IN NSEC www.example.com. A NS SOA MX TXT AAAA RRSIG NSEC DNSKEY 118 | example.com. 3600 IN RRSIG NSEC 8 2 3600 20210826152150 20210805042559 21664 example.com. 022GOnm7vHzpvZkJx2ZiwcgP2R11I9pCTHPtbnloYHi3uH6vVIXrVVQy ClDoFizUmxWPZGNjgOSdz4jODcE4lEpYVypn6XyMp2PoAQPnm0MFtH3J 8X1vWmAZnRS8xCyq2LR37j23dDLDvf2Pqrw8LKL6AlMu/Q1GrC++Ikrn 734= 119 | [TEST_END] 120 | 121 | -------------------------------------------------------------------------------- /internal/config/pages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fingertip 5 | 123 | 124 | 125 |
126 |

Fingertip

127 | 137 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 |
Handshake Resolver StatusSyncing ...
Block height--
Certificate installedChecking ...
Browser using FingertipChecking ...
DNS Interference TestChecking ...
167 |
168 | Fingertip v{{.Version}} 169 |
170 |
171 | 172 | 292 | 293 | 294 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fingertip/internal/config" 6 | "fingertip/internal/config/auto" 7 | "fingertip/internal/resolvers" 8 | "fingertip/internal/resolvers/proc" 9 | "fingertip/internal/ui" 10 | "fmt" 11 | "github.com/buffrr/letsdane" 12 | "github.com/buffrr/letsdane/resolver" 13 | "github.com/emersion/go-autostart" 14 | "github.com/pkg/browser" 15 | "log" 16 | "net/http" 17 | "os" 18 | "path" 19 | "path/filepath" 20 | "time" 21 | ) 22 | 23 | const Version = "0.0.3" 24 | 25 | type App struct { 26 | proc *proc.HNSProc 27 | server *http.Server 28 | config *config.App 29 | usrConfig *config.User 30 | proxyURL string 31 | autostart *autostart.App 32 | autostartEnabled bool 33 | } 34 | 35 | var ( 36 | appPath string 37 | fileLogger *log.Logger 38 | fileLoggerHandle *os.File 39 | ) 40 | 41 | func setupApp() *App { 42 | c, err := config.NewConfig() 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | c.Version = Version 48 | 49 | c.DNSProcPath, err = getProcPath() 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | c.DNSProcPath = path.Join(c.DNSProcPath, "hnsd") 54 | app, err := NewApp(c) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | 59 | return app 60 | } 61 | 62 | func onBoardingSeen(name string) bool { 63 | if _, err := os.Stat(name); err == nil { 64 | return true 65 | } 66 | return false 67 | } 68 | 69 | func autoConfigure(app *App, checked, onBoarded bool) bool { 70 | // TODO: delete once linux is supported 71 | if !auto.Supported() { 72 | if !onBoarded { 73 | browser.OpenURL(app.proxyURL + "/setup") 74 | } 75 | return false 76 | } 77 | 78 | autoURL := app.proxyURL + "/proxy.pac" 79 | 80 | if checked { 81 | confirm := ui.ShowYesNoDlg("Remove Fingertip configuration settings?") 82 | if confirm { 83 | auto.UninstallAutoProxy(autoURL) 84 | auto.UndoFirefoxConfiguration() 85 | _ = auto.UninstallCert(app.config.CertPath) 86 | return false 87 | } 88 | 89 | return checked 90 | } 91 | 92 | confirm := ui.ShowYesNoDlg("Would you like to automatically configure Fingertip?") 93 | if !confirm { 94 | // if this is the first time show 95 | // manual setup instructions instead 96 | if !onBoarded { 97 | browser.OpenURL(app.proxyURL + "/setup") 98 | } 99 | return false 100 | } 101 | 102 | if err := auto.InstallAutoProxy(autoURL); err != nil { 103 | ui.ShowErrorDlg(err.Error()) 104 | return false 105 | } 106 | 107 | _ = auto.ConfigureFirefox() 108 | 109 | if err := auto.InstallCert(app.config.CertPath); err != nil { 110 | // revert proxy settings 111 | auto.UninstallAutoProxy(autoURL) 112 | auto.UndoFirefoxConfiguration() 113 | 114 | ui.ShowErrorDlg(err.Error()) 115 | return false 116 | } 117 | 118 | // Enable open at login 119 | if !ui.Data.OpenAtLogin() { 120 | enable := ui.OnAutostart(false) 121 | ui.Data.SetOpenAtLogin(enable) 122 | } 123 | 124 | if time.Since(app.config.Debug.GetLastPing()) > 5*time.Second { 125 | browser.OpenURL(app.proxyURL) 126 | } 127 | return true 128 | } 129 | 130 | func main() { 131 | var err error 132 | app := setupApp() 133 | if fileLoggerHandle, err = os.OpenFile(path.Join(app.config.Path, "fingertip.logs"), 134 | os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil { 135 | log.Fatal(err) 136 | } 137 | defer fileLoggerHandle.Close() 138 | 139 | fileLogger = log.New(fileLoggerHandle, "", log.LstdFlags|log.Lshortfile) 140 | 141 | appPath, err = os.Executable() 142 | if err != nil { 143 | log.Fatalf("error reading app path: %v", err) 144 | } 145 | 146 | app.autostart.Exec = []string{appPath} 147 | if app.autostart.IsEnabled() { 148 | app.autostartEnabled = true 149 | } 150 | 151 | hnsErrCh := make(chan error) 152 | serverErrCh := make(chan error) 153 | onBoardingFilename := path.Join(app.config.Path, "init") 154 | onBoarded := onBoardingSeen(onBoardingFilename) 155 | 156 | start := func() { 157 | app.proc.Start(hnsErrCh) 158 | ui.Data.SetOptionsEnabled(true) 159 | ui.Data.SetStarted(true) 160 | 161 | go func() { 162 | serverErrCh <- app.listen() 163 | }() 164 | 165 | go func() { 166 | if onBoarded { 167 | return 168 | } 169 | 170 | autoConf := autoConfigure(app, false, false) 171 | ui.Data.SetAutoConfig(autoConf) 172 | 173 | app.config.Store.AutoConfig = autoConf 174 | go app.config.Store.Save() 175 | 176 | onBoarded = true 177 | }() 178 | } 179 | 180 | ui.OnStart = start 181 | ui.OnConfigureOS = func(checked bool) bool { 182 | res := autoConfigure(app, checked, onBoarded) 183 | app.config.Store.AutoConfig = res 184 | go app.config.Store.Save() 185 | 186 | return res 187 | } 188 | 189 | ui.OnOpenHelp = func() { 190 | browser.OpenURL(app.proxyURL) 191 | } 192 | 193 | ui.OnAutostart = func(checked bool) bool { 194 | if checked { 195 | if err := app.autostart.Disable(); err != nil { 196 | ui.ShowErrorDlg(fmt.Sprintf("error disabling open at login: %v", err)) 197 | return checked 198 | } 199 | return false 200 | } 201 | 202 | if err = app.autostart.Enable(); err != nil { 203 | ui.ShowErrorDlg(fmt.Sprintf("error enabling open at login: %v", err)) 204 | return false 205 | } 206 | 207 | return true 208 | } 209 | 210 | ticker := time.NewTicker(150 * time.Millisecond) 211 | 212 | go func() { 213 | for { 214 | select { 215 | case err := <-serverErrCh: 216 | if errors.Is(err, http.ErrServerClosed) { 217 | continue 218 | } 219 | 220 | ui.ShowErrorDlg(err.Error()) 221 | log.Printf("[ERR] app: proxy server failed: %v", err) 222 | 223 | app.stop() 224 | ui.Data.SetStarted(false) 225 | case err := <-hnsErrCh: 226 | if !app.proc.Started() { 227 | continue 228 | } 229 | 230 | // hns process crashed attempt to restart 231 | // TODO: check if port is already in use 232 | 233 | attempts := app.proc.Retries() 234 | if attempts > 9 { 235 | err := fmt.Errorf("[ERR] app: fatal error hnsd process keeps crashing err: %v", err) 236 | ui.ShowErrorDlg(err.Error()) 237 | app.stop() 238 | log.Fatal(err) 239 | } 240 | 241 | // log to a file could be useful for debugging 242 | line := fmt.Sprintf("[ERR] app: hnsd process crashed restart attempt #%d err: %v", attempts, err) 243 | log.Printf(line) 244 | fileLogger.Printf(line) 245 | 246 | // increment retries and restart process 247 | app.proc.IncrementRetries() 248 | app.proc.Stop() 249 | app.proc.Start(hnsErrCh) 250 | 251 | case <-ticker.C: 252 | if !app.proc.Started() { 253 | ui.Data.SetBlockHeight("--") 254 | app.config.Debug.SetBlockHeight(0) 255 | continue 256 | } 257 | 258 | height := app.proc.GetHeight() 259 | ui.Data.SetBlockHeight(fmt.Sprintf("#%d", height)) 260 | app.config.Debug.SetBlockHeight(height) 261 | } 262 | 263 | } 264 | }() 265 | 266 | ui.OnStop = func() { 267 | app.stop() 268 | ui.Data.SetOptionsEnabled(false) 269 | ui.Data.SetStarted(false) 270 | } 271 | 272 | ui.OnReady = func() { 273 | ui.Data.SetAutoConfigEnabled(auto.Supported()) 274 | ui.Data.SetOptionsEnabled(false) 275 | app.config.Debug.SetCheckCert(func() bool { 276 | return auto.VerifyCert(app.config.CertPath) == nil 277 | }) 278 | // update initial state 279 | ui.Data.SetOpenAtLogin(app.autostartEnabled || ui.Data.OpenAtLogin()) 280 | 281 | autoConfig := auto.Supported() && 282 | app.config.Store.AutoConfig 283 | 284 | ui.Data.SetAutoConfig(autoConfig) 285 | 286 | // start fingertip 287 | start() 288 | } 289 | 290 | ui.OnExit = func() { 291 | if fileLoggerHandle != nil { 292 | fileLoggerHandle.Close() 293 | } 294 | app.stop() 295 | } 296 | 297 | ui.Loop() 298 | } 299 | 300 | func NewApp(appConfig *config.App) (*App, error) { 301 | var err error 302 | var hnsProc *proc.HNSProc 303 | app := &App{ 304 | autostart: &autostart.App{ 305 | Name: config.AppId, 306 | DisplayName: config.AppName, 307 | Icon: "", 308 | }, 309 | } 310 | 311 | app.config = appConfig 312 | usrConfig, err := config.ReadUserConfig(appConfig.Path) 313 | if err != nil && !errors.Is(err, config.ErrUserConfigNotFound) { 314 | return nil, err 315 | } 316 | 317 | app.proxyURL = config.GetProxyURL(usrConfig.ProxyAddr) 318 | app.usrConfig = &usrConfig 319 | 320 | if hnsProc, err = proc.NewHNSProc(appConfig.DNSProcPath, usrConfig.RootAddr, usrConfig.RecursiveAddr); err != nil { 321 | return nil, err 322 | } 323 | hnsProc.SetUserAgent("fingertip:" + Version) 324 | 325 | app.proc = hnsProc 326 | 327 | app.server, err = app.newProxyServer() 328 | if err != nil { 329 | return nil, err 330 | } 331 | 332 | return app, nil 333 | } 334 | 335 | func (a *App) NewResolver() (resolver.Resolver, error) { 336 | rs, err := resolver.NewStub(a.usrConfig.RecursiveAddr) 337 | if err != nil { 338 | return nil, err 339 | } 340 | 341 | hip5 := resolvers.NewHIP5Resolver(rs, a.usrConfig.RootAddr, a.proc.Synced) 342 | ethExt, err := resolvers.NewEthereum(a.usrConfig.EthereumEndpoint) 343 | if err != nil { 344 | return nil, err 345 | } 346 | 347 | // Register HIP-5 handlers 348 | hip5.RegisterHandler("_eth", ethExt.Handler) 349 | hip5.SetQueryMiddleware(a.config.Debug.GetDNSProbeMiddleware()) 350 | a.config.Debug.SetCheckSynced(a.proc.Synced) 351 | 352 | return hip5, nil 353 | } 354 | 355 | func (a *App) listen() error { 356 | return a.server.ListenAndServe() 357 | } 358 | 359 | func (a *App) stop() { 360 | a.proc.Stop() 361 | a.server.Close() 362 | 363 | // on stop create a new server 364 | // to reset any state like old cache ... etc. 365 | var err error 366 | if a.server, err = a.newProxyServer(); err != nil { 367 | log.Fatalf("app: error creating a new proxy server: %v", err) 368 | } 369 | } 370 | 371 | func (a *App) newProxyServer() (*http.Server, error) { 372 | var err error 373 | 374 | // add a new resolver to the proxy config 375 | if a.config.Proxy.Resolver, err = a.NewResolver(); err != nil { 376 | return nil, err 377 | } 378 | 379 | // initialize a new handler 380 | h, err := a.config.Proxy.NewHandler() 381 | if err != nil { 382 | return nil, err 383 | } 384 | 385 | // copy proxy address from user specified config 386 | a.config.ProxyAddr = a.usrConfig.ProxyAddr 387 | server := &http.Server{Addr: a.config.ProxyAddr, Handler: h} 388 | return server, nil 389 | } 390 | 391 | func getProcPath() (string, error) { 392 | exe, err := os.Executable() 393 | if err != nil { 394 | return "", err 395 | } 396 | 397 | exePath := filepath.Dir(exe) 398 | return exePath, nil 399 | } 400 | 401 | func init() { 402 | // letsdane shows the version name 403 | // in the footer on errors 404 | // 0.6.1 is the version used in go.mod 405 | letsdane.Version = fmt.Sprintf("0.6.1 - fingertip (v%s)", Version) 406 | } 407 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /internal/resolvers/hip5.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fingertip/internal/resolvers/dnssec" 7 | "fmt" 8 | "github.com/buffrr/letsdane/resolver" 9 | "github.com/miekg/dns" 10 | "net" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | var errNotSynced = fmt.Errorf("error: handshake resolver not fully synced") 16 | var errHIP5NotSupported = errors.New("no supported hip-5 record found") 17 | var errBadCNAMETarget = errors.New("bad cname target") 18 | var errMaxDepthReached = errors.New("max depth reached") 19 | 20 | type hip5Handler func(ctx context.Context, qname string, qtype uint16, ns *dns.NS) ([]dns.RR, error) 21 | type QueryMiddlewareFunc func(qname string, qtype uint16) (bool, *resolver.DNSResult) 22 | 23 | type HIP5Resolver struct { 24 | handlers map[string]hip5Handler 25 | onBeforeQuery QueryMiddlewareFunc 26 | 27 | // for sending queries to a trusted root 28 | // to get hip-5 addresses 29 | rootAddr string 30 | rootClient *dns.Client 31 | syncCheck func() bool 32 | tldCache *cache 33 | keyCache *cache 34 | 35 | // stub resolver with no hip-5 support 36 | stubQuery func(ctx context.Context, name string, qtype uint16) *resolver.DNSResult 37 | *resolver.Stub 38 | 39 | // hip-5 NS recursion 40 | nsClient *dns.Client 41 | 42 | // needed for tests 43 | exchangeRoot func(ctx context.Context, m *dns.Msg, a string) (r *dns.Msg, rtt time.Duration, err error) 44 | exchange func(ctx context.Context, m *dns.Msg, a string) (r *dns.Msg, rtt time.Duration, err error) 45 | } 46 | 47 | func NewHIP5Resolver(stub *resolver.Stub, rootAddr string, syncCheck func() bool) *HIP5Resolver { 48 | h := &HIP5Resolver{} 49 | h.Stub = stub 50 | h.syncCheck = syncCheck 51 | h.handlers = make(map[string]hip5Handler) 52 | h.tldCache = newCache(30) 53 | h.keyCache = newCache(200) 54 | 55 | // using the same query function used by stub 56 | // to benefit from caching 57 | h.stubQuery = stub.DefaultResolver.Query 58 | h.Stub.DefaultResolver.Query = h.query 59 | 60 | h.rootAddr = rootAddr 61 | h.rootClient = &dns.Client{ 62 | Net: "udp", 63 | Timeout: 2 * time.Second, 64 | SingleInflight: true, 65 | } 66 | h.exchangeRoot = h.rootClient.ExchangeContext 67 | 68 | h.nsClient = &dns.Client{ 69 | Net: "udp", 70 | Timeout: 4 * time.Second, 71 | SingleInflight: true, 72 | } 73 | h.exchange = h.nsClient.ExchangeContext 74 | 75 | return h 76 | } 77 | 78 | func (h *HIP5Resolver) RegisterHandler(extension string, handler hip5Handler) { 79 | h.handlers[extension] = handler 80 | } 81 | 82 | func (h *HIP5Resolver) SetQueryMiddleware(m QueryMiddlewareFunc) { 83 | h.onBeforeQuery = m 84 | } 85 | 86 | func (h *HIP5Resolver) query(ctx context.Context, name string, qtype uint16) *resolver.DNSResult { 87 | if h.onBeforeQuery != nil { 88 | if ok, res := h.onBeforeQuery(name, qtype); ok { 89 | return res 90 | } 91 | } 92 | 93 | return h.queryInternal(ctx, name, qtype, 0) 94 | } 95 | 96 | func (h *HIP5Resolver) checkTLDCache(tld string) ([]*dns.NS, bool) { 97 | e, ok := h.tldCache.get(tld) 98 | if !ok { 99 | return nil, false 100 | } 101 | 102 | if time.Now().After(e.ttl) { 103 | h.tldCache.remove(tld) 104 | return nil, false 105 | } 106 | 107 | return e.msg.([]*dns.NS), true 108 | } 109 | 110 | func (h *HIP5Resolver) queryInternal(ctx context.Context, name string, qtype uint16, depth int) *resolver.DNSResult { 111 | if synced := h.syncCheck(); !synced { 112 | return &resolver.DNSResult{ 113 | Records: nil, 114 | Secure: false, 115 | Err: errNotSynced, 116 | } 117 | } 118 | 119 | name = dns.CanonicalName(name) 120 | tld := dns.Fqdn(LastNLabels(name, 1)) 121 | var res *resolver.DNSResult 122 | 123 | known := false 124 | if tld == "eth." { 125 | known = true 126 | } else { 127 | rrs, ok := h.checkTLDCache(tld) 128 | known = ok && len(rrs) > 0 129 | } 130 | 131 | if !known { 132 | res = h.stubQuery(ctx, name, qtype) 133 | if res.Err == nil || !errors.Is(res.Err, resolver.ErrServFail) { 134 | return res 135 | } 136 | } 137 | 138 | // Either its a known HIP-5 tld 139 | // or stub couldn't resolve it 140 | rrs, secure, errHip5 := h.attemptHIP5Resolution(ctx, tld, name, qtype, depth) 141 | if errHip5 == nil { 142 | return &resolver.DNSResult{ 143 | Records: rrs, 144 | Secure: secure, 145 | Err: nil, 146 | } 147 | } 148 | 149 | // return the original failed response 150 | // from the stub unmodified 151 | if res != nil && errHip5 == errHIP5NotSupported { 152 | return res 153 | } 154 | 155 | // name uses a hip5 ns but failed to resolve 156 | return &resolver.DNSResult{ 157 | Records: nil, 158 | Secure: false, 159 | Err: errHip5, 160 | } 161 | } 162 | 163 | func (h *HIP5Resolver) attemptHIP5Resolution(ctx context.Context, tld, qname string, qtype uint16, depth int) ([]dns.RR, bool, error) { 164 | if tld == "." { 165 | return nil, false, fmt.Errorf("no hip-5 records in root zone apex") 166 | } 167 | 168 | hip5Res, err := h.lookupExtensions(ctx, tld) 169 | if err != nil { 170 | return nil, false, fmt.Errorf("checking for hip-5 records failed: %w", err) 171 | } 172 | 173 | if len(hip5Res) > 0 { 174 | rrs, err := h.runHandlers(ctx, hip5Res, qname, qtype) 175 | if err != nil { 176 | return nil, false, fmt.Errorf("hip-5 resolution failed: %w", err) 177 | } 178 | 179 | secure := true 180 | if rrs, secure, err = h.flatten(ctx, rrs, nil, true, qname, qtype, depth); err != nil { 181 | return nil, false, err 182 | } 183 | 184 | return filterType(rrs, qtype), secure, nil 185 | } 186 | 187 | return nil, false, errHIP5NotSupported 188 | } 189 | 190 | func filterType(rrs []dns.RR, qtype uint16) []dns.RR { 191 | var other []dns.RR 192 | 193 | for _, rr := range rrs { 194 | if rr.Header().Rrtype == qtype { 195 | other = append(other, rr) 196 | } 197 | } 198 | 199 | return other 200 | } 201 | 202 | func (h *HIP5Resolver) flatten(ctx context.Context, rrs []dns.RR, extra []dns.RR, secure bool, qname string, qtype uint16, depth int) ([]dns.RR, bool, error) { 203 | if depth > 10 { 204 | return nil, false, fmt.Errorf("hip-5 resolution failed: %w", errMaxDepthReached) 205 | } 206 | 207 | var cnames []*dns.CNAME 208 | var ns []*dns.NS 209 | var ds []dns.RR 210 | 211 | for _, rr := range rrs { 212 | switch rr.(type) { 213 | case *dns.CNAME: 214 | cnames = append(cnames, rr.(*dns.CNAME)) 215 | case *dns.NS: 216 | ns = append(ns, rr.(*dns.NS)) 217 | case *dns.DS: 218 | ds = append(ds, rr) 219 | } 220 | } 221 | 222 | // response isn't secure 223 | // remove any DS records 224 | if !secure { 225 | ds = nil 226 | } 227 | 228 | if len(cnames) > 0 { 229 | return h.resolveCNAME(ctx, cnames, qname, qtype, depth) 230 | } 231 | 232 | if len(ns) > 0 { 233 | return h.resolveNS(ctx, ns, ds, extra, qname, qtype, depth) 234 | } 235 | 236 | if len(ds) > 0 { 237 | return nil, false, errors.New("error DS with no delegations") 238 | } 239 | 240 | return rrs, secure, nil 241 | } 242 | 243 | func (h *HIP5Resolver) resolveCNAME(ctx context.Context, rrs []*dns.CNAME, qname string, qtype uint16, depth int) ([]dns.RR, bool, error) { 244 | var lastErr error 245 | for _, rr := range rrs { 246 | target := dns.CanonicalName(rr.Target) 247 | if target == qname { 248 | return nil, false, errBadCNAMETarget 249 | } 250 | 251 | res := h.queryInternal(ctx, rr.Target, qtype, depth+1) 252 | if res.Err != nil { 253 | lastErr = res.Err 254 | continue 255 | } 256 | 257 | return res.Records, res.Secure, nil 258 | } 259 | 260 | return nil, false, lastErr 261 | } 262 | 263 | func getDelegatedName(rrs []*dns.NS, ds []dns.RR, qname string) (string, error) { 264 | var zone string 265 | // verify that all delegations have 266 | // the same owner name 267 | for _, rr := range rrs { 268 | if zone == "" { 269 | zone = dns.CanonicalName(rr.Header().Name) 270 | continue 271 | } 272 | 273 | if !strings.EqualFold(zone, rr.Header().Name) { 274 | return "", fmt.Errorf("got NS owner name = %s, want = %s", rr.Header().Name, zone) 275 | } 276 | } 277 | 278 | // qname must be a child of the zone 279 | if !dns.IsSubDomain(zone, qname) { 280 | return "", fmt.Errorf("qname %s isn't a child of %s", qname, zone) 281 | } 282 | 283 | // DS owner name 284 | // must match as well 285 | for _, rr := range ds { 286 | if !strings.EqualFold(zone, rr.Header().Name) { 287 | return "", fmt.Errorf("got DS owner name = %s, want %s", rr.Header().Name, zone) 288 | } 289 | } 290 | 291 | return zone, nil 292 | } 293 | 294 | func (h *HIP5Resolver) lookupNSAddr(ctx context.Context, rr *dns.NS, extra []dns.RR) ([]net.IP, error) { 295 | if rr == nil { 296 | return nil, fmt.Errorf("nil rr") 297 | } 298 | 299 | var ips []net.IP 300 | 301 | for _, glue := range extra { 302 | if !strings.EqualFold(glue.Header().Name, rr.Ns) { 303 | continue 304 | } 305 | 306 | switch t := glue.(type) { 307 | case *dns.A: 308 | ips = append(ips, t.A) 309 | case *dns.AAAA: 310 | ips = append(ips, t.AAAA) 311 | } 312 | } 313 | 314 | if len(ips) != 0 { 315 | return ips, nil 316 | } 317 | 318 | var err error 319 | if ips, _, err = h.LookupIP(ctx, "ip", rr.Ns); err != nil { 320 | return nil, err 321 | } 322 | 323 | return ips, nil 324 | } 325 | 326 | func (h *HIP5Resolver) resolveNS(ctx context.Context, rrs []*dns.NS, ds []dns.RR, extra []dns.RR, qname string, qtype uint16, depth int) ([]dns.RR, bool, error) { 327 | delegatedName, err := getDelegatedName(rrs, ds, qname) 328 | if err != nil { 329 | return nil, false, err 330 | } 331 | 332 | var msg *dns.Msg 333 | var nsIPs []net.IP 334 | 335 | for _, rr := range rrs { 336 | if nsIPs, err = h.lookupNSAddr(ctx, rr, extra); err != nil { 337 | continue 338 | } 339 | if msg, err = h.exchangeNS(ctx, nsIPs, qname, qtype); err == nil { 340 | break 341 | } 342 | } 343 | 344 | if msg == nil { 345 | return nil, false, fmt.Errorf("failed to read message") 346 | } 347 | 348 | var keys map[uint16]*dns.DNSKEY 349 | 350 | if len(ds) > 0 { 351 | if keys, err = h.queryDNSKeys(ctx, nsIPs, ds, delegatedName); err != nil { 352 | return nil, false, fmt.Errorf("dnskey error: %v", err) 353 | } 354 | } 355 | 356 | signed := len(keys) > 0 357 | var secure bool 358 | 359 | if signed { 360 | if secure, err = dnssec.Verify(msg, delegatedName, qname, qtype, keys, time.Now(), 2048); err != nil { 361 | return nil, false, fmt.Errorf("dnssec verify error: %v", err) 362 | } 363 | } 364 | 365 | // limit recursion depth 366 | depth++ 367 | 368 | if len(msg.Answer) > 0 { 369 | return h.flatten(ctx, msg.Answer, nil, secure, qname, qtype, depth) 370 | } 371 | 372 | return h.flatten(ctx, msg.Ns, msg.Extra, secure, qname, qtype, depth) 373 | } 374 | 375 | func (h *HIP5Resolver) queryDNSKeys(ctx context.Context, ips []net.IP, ds []dns.RR, delegatedName string) (map[uint16]*dns.DNSKEY, error) { 376 | if entry, ok := h.keyCache.get(delegatedName); ok { 377 | if time.Now().Before(entry.ttl) { 378 | msg := new(dns.Msg) 379 | msg.Rcode = dns.RcodeSuccess 380 | msg.Answer = entry.msg.([]dns.RR) 381 | 382 | keys, err := dnssec.VerifyDNSKeys(delegatedName, msg, ds, time.Now(), 2048) 383 | if err == nil { 384 | return keys, nil 385 | } 386 | } 387 | h.keyCache.remove(delegatedName) 388 | } 389 | 390 | msg, err := h.exchangeNS(ctx, ips, delegatedName, dns.TypeDNSKEY) 391 | if err != nil { 392 | return nil, err 393 | } 394 | 395 | keys, err := dnssec.VerifyDNSKeys(delegatedName, msg, ds, time.Now(), 2048) 396 | if err != nil { 397 | return nil, err 398 | } 399 | if len(keys) == 0 { 400 | return nil, nil 401 | } 402 | 403 | h.keyCache.set(delegatedName, &entry{ 404 | msg: msg.Answer, 405 | ttl: time.Now().Add(getTTL(msg.Answer)), 406 | }) 407 | 408 | return keys, nil 409 | } 410 | 411 | func (h *HIP5Resolver) exchangeNS(ctx context.Context, ips []net.IP, qname string, qtype uint16) (res *dns.Msg, err error) { 412 | m := new(dns.Msg) 413 | m.SetQuestion(qname, qtype) 414 | m.RecursionDesired = false 415 | m.CheckingDisabled = true 416 | m.SetEdns0(4096, true) 417 | 418 | for _, ip := range ips { 419 | if res, _, err = h.exchange(ctx, m, ip.String()+":53"); err != nil { 420 | continue 421 | } 422 | 423 | if res.Truncated { 424 | err = errors.New("response truncated") 425 | continue 426 | } 427 | 428 | return 429 | } 430 | 431 | return 432 | } 433 | 434 | func (h *HIP5Resolver) runHandlers(ctx context.Context, extensions []*dns.NS, qname string, qtype uint16) ([]dns.RR, error) { 435 | var lastErr error 436 | var res []dns.RR 437 | 438 | for _, rr := range extensions { 439 | tld := LastNLabels(rr.Ns, 1) 440 | if handler, ok := h.handlers[tld]; ok { 441 | res, lastErr = handler(ctx, qname, qtype, rr) 442 | 443 | if lastErr == nil { 444 | return res, nil 445 | } 446 | } 447 | } 448 | 449 | return nil, lastErr 450 | } 451 | 452 | func (h *HIP5Resolver) lookupExtensions(ctx context.Context, tld string) ([]*dns.NS, error) { 453 | if !dns.IsFqdn(tld) { 454 | return nil, errors.New("tld must be fqdn") 455 | } 456 | 457 | if tld == "eth." { 458 | return ethNS, nil 459 | } 460 | 461 | if rrs, ok := h.checkTLDCache(tld); ok { 462 | return rrs, nil 463 | } 464 | 465 | m := new(dns.Msg) 466 | m.SetQuestion(tld, dns.TypeNS) 467 | m.RecursionDesired = false 468 | m.SetEdns0(4096, true) 469 | 470 | r, _, err := h.exchangeRoot(ctx, m, h.rootAddr) 471 | if err != nil { 472 | return nil, err 473 | } 474 | 475 | if r.Rcode != dns.RcodeSuccess { 476 | return nil, fmt.Errorf("hip-5 lookup failed with rcode %d", r.Rcode) 477 | } 478 | 479 | if r.Truncated { 480 | return nil, errors.New("response truncated") 481 | } 482 | 483 | var answer []*dns.NS 484 | 485 | for _, rr := range r.Ns { 486 | if ns, ok := rr.(*dns.NS); ok { 487 | ending := LastNLabels(ns.Ns, 1) 488 | 489 | // include supported HIP-5 extensions only 490 | if _, ok := h.handlers[ending]; ok { 491 | answer = append(answer, ns) 492 | } 493 | } 494 | } 495 | 496 | // only cache positive answers 497 | if len(answer) > 0 { 498 | ttl := getTTL(nsToRR(answer)) 499 | h.tldCache.set(tld, &entry{ 500 | msg: answer, 501 | ttl: time.Now().Add(ttl), 502 | }) 503 | } 504 | 505 | return answer, nil 506 | } 507 | -------------------------------------------------------------------------------- /internal/resolvers/dnssec/testdata/val_ns.forever.txt: -------------------------------------------------------------------------------- 1 | [ZONE] origin: ns.forever., time: 20210823000000 2 | [TRUST_ANCHORS] 3 | ns.forever. 300 IN DS 21761 13 2 3B606B0AFF27AD10E5E8903D5BF3CD36F7ABCA44D1BA6F9C59099372936A9845 4 | 5 | [DNSKEYS] 6 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 10258 7 | ;; flags: qr aa rd; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1 8 | ;; WARNING: recursion requested but not available 9 | 10 | ;; OPT PSEUDOSECTION: 11 | ; EDNS: version: 0, flags: do; udp: 1232 12 | ;; QUESTION SECTION: 13 | ;ns.forever. IN DNSKEY 14 | 15 | ;; ANSWER SECTION: 16 | ns.forever. 300 IN DNSKEY 256 3 13 6PEpjcur7mZVVivbBuxiWxVkdmTeOqdTYNu6u7QGRSSV+/Fpit0lLfuc Tm0PVaG60K6YjO3cRQENP8yWYjrGQg== 17 | ns.forever. 300 IN DNSKEY 257 3 13 P8zBKmseFGP/5hv2V1l6X0COy/GSKW/c/ExWbU90GODLVsdytNtv40C7 7zgGPlgH0WlDOvEuoRdBVIBB9NB2PA== 18 | ns.forever. 300 IN RRSIG DNSKEY 13 2 300 20210830204245 20210816191245 21761 ns.forever. ys8h+zF2JL+e0SJJYIxEari1ojRygQXUieIscjHfLsEYL65S8comQ1mb dpTaVYQzP9gAnCTaXWugQwPSRZBdVw== 19 | 20 | 21 | [TEST_BEGIN] name: verify nxdomain 22 | [INPUT] 23 | ;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 40620 24 | ;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 6, ADDITIONAL: 1 25 | ;; WARNING: recursion requested but not available 26 | ;; OPT PSEUDOSECTION: 27 | ; EDNS: version: 0, flags: do; udp: 1232 28 | ;; QUESTION SECTION: 29 | ;doesntexist.ns.forever. IN A 30 | ;; AUTHORITY SECTION: 31 | delegate-secure.ns.forever. 300 IN NSEC *.wild.ns.forever. NS DS RRSIG NSEC 32 | ns.forever. 300 IN NSEC _443._tcp.ns.forever. A NS SOA RRSIG NSEC DNSKEY CDS CDNSKEY 33 | delegate-secure.ns.forever. 300 IN RRSIG NSEC 13 3 300 20210830204245 20210816191245 45607 ns.forever. JC6x2z2WutUOIiBPAze4O4WRVcwBG+qi2077mWf0H4aIBKaoLRf6KmqY uqw/BjNgXJnjysWjYrhdu0IO+Gbniw== 34 | ns.forever. 300 IN RRSIG NSEC 13 2 300 20210830204245 20210816191245 45607 ns.forever. /JqBC1fWWusc6k+SwOGBmcdwJ8JK0eG3xfoRWfMT2ZfO6FwYwW4KRDs3 LEbfEvwFcjuTnQSrDb0rBpJzJdsMtA== 35 | 36 | [RESULT] secure: 1, bogus: 0 37 | [TEST_END] 38 | 39 | 40 | [TEST_BEGIN] name: verify nodata 41 | [INPUT] 42 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 51881 43 | ;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1 44 | ;; WARNING: recursion requested but not available 45 | 46 | ;; OPT PSEUDOSECTION: 47 | ; EDNS: version: 0, flags: do; udp: 1232 48 | ;; QUESTION SECTION: 49 | ;ns.forever. IN TXT 50 | 51 | ;; AUTHORITY SECTION: 52 | ns.forever. 300 IN NSEC _443._tcp.ns.forever. A NS SOA RRSIG NSEC DNSKEY CDS CDNSKEY 53 | ns.forever. 300 IN RRSIG NSEC 13 2 300 20210830204245 20210816191245 45607 ns.forever. /JqBC1fWWusc6k+SwOGBmcdwJ8JK0eG3xfoRWfMT2ZfO6FwYwW4KRDs3 LEbfEvwFcjuTnQSrDb0rBpJzJdsMtA== 54 | 55 | [RESULT] secure: 1, bogus: 0 56 | [TEST_END] 57 | 58 | 59 | [TEST_BEGIN] name: verify positive answer 60 | [INPUT] 61 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 18805 62 | ;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 63 | ;; WARNING: recursion requested but not available 64 | 65 | ;; OPT PSEUDOSECTION: 66 | ; EDNS: version: 0, flags: do; udp: 1232 67 | ;; QUESTION SECTION: 68 | ;ns.forever. IN A 69 | 70 | ;; ANSWER SECTION: 71 | ns.forever. 300 IN A 147.182.253.76 72 | ns.forever. 300 IN RRSIG A 13 2 300 20210830204245 20210816191245 45607 ns.forever. zN7FXBmKAa5OKJtDo6mCftGe/K4v7C67h9qeHC1N6VNWvIU35ZXGJhUZ 8sISihEKm1WvhgpWlahxdOWH7rCnBA== 73 | 74 | [RESULT] secure: 1, bogus: 0 75 | [TEST_END] 76 | 77 | [TEST_BEGIN] name: sanitize signed rrs 78 | [INPUT] 79 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 18805 80 | ;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 81 | ;; WARNING: recursion requested but not available 82 | 83 | ;; OPT PSEUDOSECTION: 84 | ; EDNS: version: 0, flags: do; udp: 1232 85 | ;; QUESTION SECTION: 86 | ;ns.forever. IN A 87 | 88 | ;; ANSWER SECTION: 89 | ns.forever. 300 IN A 147.182.253.76 90 | ns.forever. 300 IN RRSIG A 13 2 300 20210830204245 20210816191245 45607 ns.forever. zN7FXBmKAa5OKJtDo6mCftGe/K4v7C67h9qeHC1N6VNWvIU35ZXGJhUZ 8sISihEKm1WvhgpWlahxdOWH7rCnBA== 91 | ns.forever. 300 IN DNSKEY 256 3 13 6PEpjcur7mZVVivbBuxiWxVkdmTeOqdTYNu6u7QGRSSV+/Fpit0lLfuc Tm0PVaG60K6YjO3cRQENP8yWYjrGQg== 92 | ns.forever. 300 IN DNSKEY 257 3 13 P8zBKmseFGP/5hv2V1l6X0COy/GSKW/c/ExWbU90GODLVsdytNtv40C7 7zgGPlgH0WlDOvEuoRdBVIBB9NB2PA== 93 | ns.forever. 300 IN RRSIG DNSKEY 13 2 300 20210830204245 20210816191245 21761 ns.forever. ys8h+zF2JL+e0SJJYIxEari1ojRygQXUieIscjHfLsEYL65S8comQ1mb dpTaVYQzP9gAnCTaXWugQwPSRZBdVw== 94 | _443._tcp.ns.forever. 300 IN TLSA 3 1 1 F990C4CF0DB6A00465785870DA95A9F696F983DD41147AC149BD72BD 5C10B07A 95 | _443._tcp.ns.forever. 300 IN RRSIG TLSA 13 4 300 20210907185720 20210824172720 45607 ns.forever. bOWtb7rEchu4Jc42cjeokG/OhX/QmQKDbI4/70P6ZNhGdHKE7x++7XTb NobpOecOeMh13NgIy+SSFsUxlbXj7A== 96 | [RESULT] secure: 1, bogus: 0 97 | [VERIFY_MESSAGE] 98 | ;; ANSWER SECTION: 99 | ns.forever. 300 IN A 147.182.253.76 100 | ns.forever. 300 IN RRSIG A 13 2 300 20210830204245 20210816191245 45607 ns.forever. zN7FXBmKAa5OKJtDo6mCftGe/K4v7C67h9qeHC1N6VNWvIU35ZXGJhUZ 8sISihEKm1WvhgpWlahxdOWH7rCnBA== 101 | [TEST_END] 102 | 103 | 104 | [TEST_BEGIN] name: positive answer with wildcards nsec exact match 105 | [INPUT] 106 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 41877 107 | ;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 2, ADDITIONAL: 1 108 | ;; WARNING: recursion requested but not available 109 | 110 | ;; OPT PSEUDOSECTION: 111 | ; EDNS: version: 0, flags: do; udp: 1232 112 | ;; QUESTION SECTION: 113 | ;example.wild.ns.forever. IN A 114 | 115 | ;; ANSWER SECTION: 116 | example.wild.ns.forever. 300 IN A 147.182.253.76 117 | example.wild.ns.forever. 300 IN RRSIG A 13 3 300 20210830204245 20210816191245 45607 ns.forever. Lb8ICn2XB3eFes2SW2p0rY7uNx/1TpYg70ulE0T9NeRtOEwMj1nqW79N EP14vob5fbNNY/wI+ZnAtvh7f06aeg== 118 | 119 | ;; AUTHORITY SECTION: 120 | *.wild.ns.forever. 300 IN NSEC ns.forever. A RRSIG NSEC TLSA 121 | *.wild.ns.forever. 300 IN RRSIG NSEC 13 3 300 20210830204245 20210816191245 45607 ns.forever. h0FURI6vcAB4WD9uSjTml5H5EGkIOkJgpuNtu8CqXX/UbCfxD918Ky96 5DF+Pa8AUvnEOO/jgcl72vPq71CLnA== 122 | [RESULT] secure: 1, bogus: 0 123 | [TEST_END] 124 | 125 | [TEST_BEGIN] name: positive answer with wildcards nsec doesn't match owner 126 | [INPUT] 127 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 41658 128 | ;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 2, ADDITIONAL: 1 129 | ;; WARNING: recursion requested but not available 130 | 131 | ;; OPT PSEUDOSECTION: 132 | ; EDNS: version: 0, flags: do; udp: 1232 133 | ;; QUESTION SECTION: 134 | ;!.wild.ns.forever. IN A 135 | 136 | ;; ANSWER SECTION: 137 | !.wild.ns.forever. 300 IN A 147.182.253.76 138 | !.wild.ns.forever. 300 IN RRSIG A 13 3 300 20210830204245 20210816191245 45607 ns.forever. Lb8ICn2XB3eFes2SW2p0rY7uNx/1TpYg70ulE0T9NeRtOEwMj1nqW79N EP14vob5fbNNY/wI+ZnAtvh7f06aeg== 139 | 140 | ;; AUTHORITY SECTION: 141 | delegate-secure.ns.forever. 300 IN NSEC *.wild.ns.forever. NS DS RRSIG NSEC 142 | delegate-secure.ns.forever. 300 IN RRSIG NSEC 13 3 300 20210830204245 20210816191245 45607 ns.forever. JC6x2z2WutUOIiBPAze4O4WRVcwBG+qi2077mWf0H4aIBKaoLRf6KmqY uqw/BjNgXJnjysWjYrhdu0IO+Gbniw== 143 | 144 | [RESULT] secure: 1, bogus: 0 145 | [VERIFY_MESSAGE] 146 | ; check that both answer and authority sections are present 147 | ; nsec not matching owner name shouldn't be omitted 148 | ;; ANSWER SECTION: 149 | !.wild.ns.forever. 300 IN A 147.182.253.76 150 | !.wild.ns.forever. 300 IN RRSIG A 13 3 300 20210830204245 20210816191245 45607 ns.forever. Lb8ICn2XB3eFes2SW2p0rY7uNx/1TpYg70ulE0T9NeRtOEwMj1nqW79N EP14vob5fbNNY/wI+ZnAtvh7f06aeg== 151 | 152 | ;; AUTHORITY SECTION: 153 | delegate-secure.ns.forever. 300 IN NSEC *.wild.ns.forever. NS DS RRSIG NSEC 154 | delegate-secure.ns.forever. 300 IN RRSIG NSEC 13 3 300 20210830204245 20210816191245 45607 ns.forever. JC6x2z2WutUOIiBPAze4O4WRVcwBG+qi2077mWf0H4aIBKaoLRf6KmqY uqw/BjNgXJnjysWjYrhdu0IO+Gbniw== 155 | [TEST_END] 156 | 157 | 158 | [TEST_BEGIN] name: nodata empty non-terminal 159 | [INPUT] 160 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 46554 161 | ;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 6, ADDITIONAL: 1 162 | ;; WARNING: recursion requested but not available 163 | 164 | ;; OPT PSEUDOSECTION: 165 | ; EDNS: version: 0, flags: do; udp: 1232 166 | ;; QUESTION SECTION: 167 | ;wild.ns.forever. IN A 168 | 169 | ;; AUTHORITY SECTION: 170 | delegate-secure.ns.forever. 300 IN NSEC *.wild.ns.forever. NS DS RRSIG NSEC 171 | *.wild.ns.forever. 300 IN NSEC ns.forever. A RRSIG NSEC TLSA 172 | delegate-secure.ns.forever. 300 IN RRSIG NSEC 13 3 300 20210830204245 20210816191245 45607 ns.forever. JC6x2z2WutUOIiBPAze4O4WRVcwBG+qi2077mWf0H4aIBKaoLRf6KmqY uqw/BjNgXJnjysWjYrhdu0IO+Gbniw== 173 | *.wild.ns.forever. 300 IN RRSIG NSEC 13 3 300 20210830204245 20210816191245 45607 ns.forever. h0FURI6vcAB4WD9uSjTml5H5EGkIOkJgpuNtu8CqXX/UbCfxD918Ky96 5DF+Pa8AUvnEOO/jgcl72vPq71CLnA== 174 | 175 | [RESULT] secure: 1, bogus: 0 176 | [TEST_END] 177 | 178 | [TEST_BEGIN] name: nodata but the type exists 179 | [INPUT] 180 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 46554 181 | ;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 6, ADDITIONAL: 1 182 | ;; WARNING: recursion requested but not available 183 | 184 | ;; OPT PSEUDOSECTION: 185 | ; EDNS: version: 0, flags: do; udp: 1232 186 | ;; QUESTION SECTION: 187 | ;cool.ns.forever. IN A 188 | 189 | ;; AUTHORITY SECTION: 190 | delegate-secure.ns.forever. 300 IN NSEC *.wild.ns.forever. NS DS RRSIG NSEC 191 | *.wild.ns.forever. 300 IN NSEC ns.forever. A RRSIG NSEC TLSA 192 | delegate-secure.ns.forever. 300 IN RRSIG NSEC 13 3 300 20210830204245 20210816191245 45607 ns.forever. JC6x2z2WutUOIiBPAze4O4WRVcwBG+qi2077mWf0H4aIBKaoLRf6KmqY uqw/BjNgXJnjysWjYrhdu0IO+Gbniw== 193 | *.wild.ns.forever. 300 IN RRSIG NSEC 13 3 300 20210830204245 20210816191245 45607 ns.forever. h0FURI6vcAB4WD9uSjTml5H5EGkIOkJgpuNtu8CqXX/UbCfxD918Ky96 5DF+Pa8AUvnEOO/jgcl72vPq71CLnA== 194 | 195 | [RESULT] secure: 0, bogus: 1 196 | [TEST_END] 197 | 198 | [TEST_BEGIN] name: verify insecure delegation 199 | [INPUT] 200 | ; Test delegation proof 201 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 53444 202 | ;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 3, ADDITIONAL: 3 203 | ;; WARNING: recursion requested but not available 204 | 205 | ;; OPT PSEUDOSECTION: 206 | ; EDNS: version: 0, flags: do; udp: 1232 207 | ;; QUESTION SECTION: 208 | ;blog.ns.forever. IN A 209 | 210 | ;; AUTHORITY SECTION: 211 | blog.ns.forever. 300 IN NS cool.ns.forever. 212 | blog.ns.forever. 300 IN NSEC cname.ns.forever. NS RRSIG NSEC 213 | blog.ns.forever. 300 IN RRSIG NSEC 13 3 300 20210830204245 20210816191245 45607 ns.forever. +LJ8aURGptiV0afOaeGHNa3VNzJyuIVFm2RGe3RaIwh9qbUeULyQr+Hp zUIzjh2wmcEWO3H9CD5WnQGW9JUBrA== 214 | 215 | ;; ADDITIONAL SECTION: 216 | cool.ns.forever. 300 IN A 127.0.0.1 217 | cool.ns.forever. 300 IN RRSIG A 13 3 300 20210830204245 20210816191245 45607 ns.forever. Aru7Sqm7aEX6I/uKwKF6MPnkZItbm9UdHYkVGNCpkOI484IC6l8QAWlg S8avbcbd3Q627VPccxrCB0+Sy8PZZw==[RESULT] secure: 1, bogus: 0 218 | [RESULT] secure: 1, bogus: 0 219 | [TEST_END] 220 | 221 | [TEST_BEGIN] name: verify insecure delegation qname child of owner 222 | [INPUT] 223 | ; Test delegation proof 224 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 53444 225 | ;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 3, ADDITIONAL: 3 226 | ;; WARNING: recursion requested but not available 227 | 228 | ;; OPT PSEUDOSECTION: 229 | ; EDNS: version: 0, flags: do; udp: 1232 230 | ;; QUESTION SECTION: 231 | ;static.blog.ns.forever. IN A 232 | 233 | ;; AUTHORITY SECTION: 234 | blog.ns.forever. 300 IN NS cool.ns.forever. 235 | blog.ns.forever. 300 IN NSEC cname.ns.forever. NS RRSIG NSEC 236 | blog.ns.forever. 300 IN RRSIG NSEC 13 3 300 20210830204245 20210816191245 45607 ns.forever. +LJ8aURGptiV0afOaeGHNa3VNzJyuIVFm2RGe3RaIwh9qbUeULyQr+Hp zUIzjh2wmcEWO3H9CD5WnQGW9JUBrA== 237 | 238 | ;; ADDITIONAL SECTION: 239 | cool.ns.forever. 300 IN A 127.0.0.1 240 | cool.ns.forever. 300 IN RRSIG A 13 3 300 20210830204245 20210816191245 45607 ns.forever. Aru7Sqm7aEX6I/uKwKF6MPnkZItbm9UdHYkVGNCpkOI484IC6l8QAWlg S8avbcbd3Q627VPccxrCB0+Sy8PZZw==[RESULT] secure: 1, bogus: 0 241 | [RESULT] secure: 1, bogus: 0 242 | [TEST_END] 243 | 244 | 245 | [TEST_BEGIN] name: bad wildcard substitution name exists, time: 20210824172721 246 | [INPUT] 247 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 37645 248 | ;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 2, ADDITIONAL: 1 249 | ;; WARNING: recursion requested but not available 250 | 251 | ;; OPT PSEUDOSECTION: 252 | ; EDNS: version: 0, flags: do; udp: 1232 253 | ;; QUESTION SECTION: 254 | ;f.wild.ns.forever. IN TLSA 255 | 256 | ;; ANSWER SECTION: 257 | f.wild.ns.forever. 300 IN TLSA 3 1 1 F990C4CF0DB6A00465785870DA95A9F696F983DD41147AC149BD72BD 5C10B07A 258 | f.wild.ns.forever. 300 IN RRSIG TLSA 13 3 300 20210907185720 20210824172720 45607 ns.forever. ZdDptniYEFHymvbEmvbwUOvJ/g5LjG5MiUDDUYyZciKtE87YZRA1B4db cAaybCpqGPczKjlsderz9Mu1gQRSHQ== 259 | 260 | ;; AUTHORITY SECTION: 261 | a.wild.ns.forever. 300 IN NSEC exists.wild.ns.forever. RRSIG NSEC TLSA 262 | a.wild.ns.forever. 300 IN RRSIG NSEC 13 4 300 20210907185720 20210824172720 45607 ns.forever. SHqlUqleahRO/Zh8jXnbgWZut+q9lAbEE0lYa7tCO2xQkqspkz2dgf+5 IZH0yhrwSChD8gHkXtoGHgWlREvhJA== 263 | 264 | [RESULT] secure: 0, bogus: 1 265 | [TEST_END] 266 | 267 | -------------------------------------------------------------------------------- /internal/resolvers/dnssec/dnssec_test.go: -------------------------------------------------------------------------------- 1 | package dnssec 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "github.com/miekg/dns" 7 | "io/ioutil" 8 | "os" 9 | "path" 10 | "sort" 11 | "strconv" 12 | "strings" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | type testHDR struct { 18 | verifyDNSKeys bool 19 | zone string 20 | time time.Time 21 | dnsKeys string 22 | anchors string 23 | minRSA int 24 | } 25 | 26 | type testCase struct { 27 | inputMsg string 28 | filteredMsg string 29 | name string 30 | time time.Time 31 | secure bool 32 | bogus bool 33 | } 34 | 35 | func TestVerify(t *testing.T) { 36 | wd, _ := os.Getwd() 37 | 38 | dir := path.Join(wd, "testdata") 39 | files, err := ioutil.ReadDir(dir) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | for _, file := range files { 45 | if !strings.HasPrefix(file.Name(), "val_") { 46 | continue 47 | } 48 | 49 | f, err := os.Open(path.Join(dir, file.Name())) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | parseTestFile(t, f, runTest) 55 | } 56 | } 57 | 58 | func Test_filterDigest(t *testing.T) { 59 | dsSet := ` 60 | ns.forever. 0 IN DS 21761 13 1 004ad4545ffc78c2a19853b4dd5b6b1db96e1c8a 61 | ns.forever. 0 IN DS 21761 13 2 3b606b0aff27ad10e5e8903d5bf3cd36f7abca44d1ba6f9c59099372936a9845 62 | ns.forever. 0 IN DS 21761 13 4 1f20aed75154bd655ac7f9693744afa8a2e97dcffaec0bfd22363d5f70969292cadd001630e9962404a51bb51e35d07e 63 | ` 64 | want := ` 65 | ns.forever. 0 IN DS 21761 13 4 1f20aed75154bd655ac7f9693744afa8a2e97dcffaec0bfd22363d5f70969292cadd001630e9962404a51bb51e35d07e 66 | ` 67 | 68 | rrs := zoneToRecords(dsSet) 69 | set, err := filterDS("ns.forever.", rrs) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | 74 | got := recordsToZone(dsToRR(set)) 75 | want = recordsToZone(zoneToRecords(want)) 76 | 77 | if got != want { 78 | t.Fatalf("got dsSet = %s \n want = %s", got, want) 79 | } 80 | 81 | } 82 | 83 | func Test_canonicalNameCompare2(t *testing.T) { 84 | // same tests used by unbound 85 | tests := []struct { 86 | name1 string 87 | name2 string 88 | result int 89 | }{ 90 | { 91 | "", 92 | "", 93 | 0, 94 | }, 95 | { 96 | "example.net.", 97 | "example.net.", 98 | 0, 99 | }, 100 | { 101 | "test.example.net.", 102 | "test.example.net.", 103 | 0, 104 | }, 105 | { 106 | "com.", 107 | "", 108 | 1, 109 | }, 110 | { 111 | "", 112 | "com.", 113 | -1, 114 | }, 115 | { 116 | "example.com.", 117 | "com.", 118 | 1, 119 | }, 120 | { 121 | "com.", 122 | "example.com.", 123 | -1, 124 | }, 125 | { 126 | "example.com.", 127 | "", 128 | 1, 129 | }, 130 | { 131 | "", 132 | "example.com.", 133 | -1, 134 | }, 135 | { 136 | "com.", 137 | "net.", 138 | -1, 139 | }, 140 | { 141 | "net.", 142 | "com.", 143 | 1, 144 | }, 145 | { 146 | "net.", 147 | "org.", 148 | -1, 149 | }, 150 | { 151 | "neta.", 152 | "net.", 153 | 1, 154 | }, 155 | { 156 | "ne.", 157 | "neta.", 158 | -1, 159 | }, 160 | { 161 | "aag.example.net.", 162 | "bla.example.net.", 163 | -1, 164 | }, 165 | } 166 | 167 | for _, test := range tests { 168 | t.Run(test.name1+","+test.name2, func(t *testing.T) { 169 | v, err := canonicalNameCompare(test.name1, test.name2) 170 | if err != nil { 171 | t.Fatal(err) 172 | } 173 | if v != test.result { 174 | t.Fatalf("got result = %v want %v", v, test.result) 175 | } 176 | }) 177 | } 178 | } 179 | 180 | func Test_canonicalNameCompare(t *testing.T) { 181 | zone := `\001.z.example. 300 IN A 127.0.0.1 182 | \200.z.example. 300 IN A 127.0.0.1 183 | example. 300 IN A 127.0.0.1 184 | a.example. 300 IN A 127.0.0.1 185 | yljkjljk.a.example. 300 IN A 127.0.0.1 186 | Z.a.example. 300 IN A 127.0.0.1 187 | zABC.a.EXAMPLE. 300 IN A 127.0.0.1 188 | *.z.example. 300 IN A 127.0.0.1 189 | z.example. 300 IN A 127.0.0.1 190 | ` 191 | want := `example. 300 IN A 127.0.0.1 192 | a.example. 300 IN A 127.0.0.1 193 | yljkjljk.a.example. 300 IN A 127.0.0.1 194 | Z.a.example. 300 IN A 127.0.0.1 195 | zABC.a.EXAMPLE. 300 IN A 127.0.0.1 196 | z.example. 300 IN A 127.0.0.1 197 | \001.z.example. 300 IN A 127.0.0.1 198 | *.z.example. 300 IN A 127.0.0.1 199 | \200.z.example. 300 IN A 127.0.0.1 200 | ` 201 | 202 | rrs := zoneToRecords(zone) 203 | sort.Slice(rrs, func(i, j int) bool { 204 | res, err := canonicalNameCompare(rrs[i].Header().Name, rrs[j].Header().Name) 205 | if err != nil { 206 | panic(err) 207 | } 208 | 209 | return res < 0 210 | }) 211 | 212 | got := recordsToZone(rrs) 213 | if got != want { 214 | t.Fatalf("got zone = \n`%s`\nwant zone = \n`%s`\n", got, want) 215 | } 216 | 217 | if res, err := canonicalNameCompare("", "."); err != nil || res != 0 { 218 | t.Fatal("failed to compare root label") 219 | } 220 | 221 | if res, _ := canonicalNameCompare("\\001.", "."); res != 1 { 222 | t.Fatal("root label is smaller than other labels") 223 | } 224 | 225 | if res, err := canonicalNameCompare("eXampl\\069.", "exam\\112le"); err != nil || res != 0 { 226 | t.Fatal("comparison must be case/formatting insensitive") 227 | } 228 | 229 | if res, _ := canonicalNameCompare(strings.Repeat("a", 63)+".example", "example"); res != 1 { 230 | t.Fatal("could not compare long labels") 231 | } 232 | 233 | if _, err := canonicalNameCompare(strings.Repeat("a", 64)+".example", "example"); err == nil { 234 | t.Fatal("got no error on invalid label length") 235 | } 236 | } 237 | 238 | func sectionsMatch(t *testing.T, name string, a, b []dns.RR) { 239 | secA := recordsToZoneSorted(a) 240 | secB := recordsToZoneSorted(b) 241 | 242 | if secA != secB { 243 | t.Fatalf("%s section dont't match \n%s != \n%s", name, secA, secB) 244 | } 245 | } 246 | 247 | func runTest(t *testing.T, hdr *testHDR, tc *testCase) { 248 | dsSet := zoneToRecords(hdr.anchors) 249 | dnskeyMsg, err := stringToMsg(hdr.dnsKeys) 250 | if err != nil { 251 | t.Fatal(err) 252 | } 253 | 254 | var keys map[uint16]*dns.DNSKEY 255 | verifyMessage := false 256 | 257 | if strings.TrimSpace(tc.filteredMsg) != "" { 258 | verifyMessage = true 259 | } 260 | 261 | filtered, err := stringToMsg(tc.filteredMsg) 262 | if err != nil { 263 | t.Fatal(err) 264 | } 265 | 266 | if hdr.verifyDNSKeys { 267 | t.Run("verify dnskeys", func(t *testing.T) { 268 | var err error 269 | keys, err = VerifyDNSKeys(hdr.zone, dnskeyMsg, dsSet, hdr.time, hdr.minRSA) 270 | if err != nil { 271 | t.Fatal(err) 272 | } 273 | }) 274 | 275 | } else { 276 | keys = make(map[uint16]*dns.DNSKEY) 277 | 278 | for _, rr := range dnskeyMsg.Ns { 279 | key := rr.(*dns.DNSKEY) 280 | keys[key.KeyTag()] = key 281 | } 282 | } 283 | 284 | testMsg, err := stringToMsg(tc.inputMsg) 285 | if err != nil { 286 | t.Fatal(err) 287 | } 288 | 289 | currTime := hdr.time 290 | if !tc.time.IsZero() { 291 | currTime = tc.time 292 | } 293 | 294 | ok, err := Verify(testMsg, hdr.zone, testMsg.Question[0].Name, testMsg.Question[0].Qtype, keys, currTime, hdr.minRSA) 295 | if tc.bogus { 296 | if err == nil { 297 | t.Fatalf("got no error, want bogus") 298 | } 299 | } else if err != nil { 300 | t.Fatal(err) 301 | } else if tc.secure != ok { 302 | t.Fatalf("got secure = %v, want %v", ok, tc.secure) 303 | } 304 | 305 | if verifyMessage { 306 | sectionsMatch(t, "authority", testMsg.Ns, filtered.Ns) 307 | sectionsMatch(t, "answer", testMsg.Answer, filtered.Answer) 308 | sectionsMatch(t, "additional", testMsg.Extra, filtered.Extra) 309 | } 310 | } 311 | 312 | func parseTestFile(t *testing.T, f *os.File, run func(t *testing.T, hdr *testHDR, tc *testCase)) { 313 | sc := bufio.NewScanner(f) 314 | 315 | var begin bool 316 | 317 | var th testHDR 318 | var tc testCase 319 | 320 | scanning := -1 321 | 322 | for sc.Scan() { 323 | line := strings.TrimSpace(sc.Text()) 324 | switch { 325 | case strings.HasPrefix(line, "[ZONE]"): 326 | th.zone = "" 327 | parseKeyValPairs(line[6:], ",", func(key string, val string) { 328 | if key == "origin" { 329 | th.zone = val 330 | } 331 | 332 | if key == "time" { 333 | t, err := dns.StringToTime(val) 334 | if err != nil { 335 | panic("unable to parse time used in test: " + err.Error()) 336 | } 337 | 338 | th.time = time.Unix(int64(t), 0) 339 | } 340 | }) 341 | continue 342 | case line == "[TRUST_ANCHORS]": 343 | th.anchors = "" 344 | scanning = 1 345 | continue 346 | case strings.HasPrefix(line, "[DNSKEYS]"): 347 | th.dnsKeys = "" 348 | scanning = 2 349 | th.minRSA = DefaultMinRSAKeySize 350 | th.verifyDNSKeys = true 351 | 352 | parseKeyValPairs(line[9:], ",", func(key string, val string) { 353 | if key == "min_rsa_keysize" { 354 | var err error 355 | if th.minRSA, err = strconv.Atoi(val); err != nil { 356 | t.Fatal(err) 357 | } 358 | } 359 | if key == "verify" { 360 | th.verifyDNSKeys = val == "1" 361 | } 362 | }) 363 | 364 | continue 365 | case line == "[INPUT]": 366 | scanning = 3 367 | continue 368 | case line == "[VERIFY_MESSAGE]": 369 | scanning = 4 370 | continue 371 | case strings.HasPrefix(line, "[TEST_BEGIN]"): 372 | if begin { 373 | panic("test didn't end") 374 | } 375 | begin = true 376 | parseKeyValPairs(line[12:], ",", func(key string, val string) { 377 | if key == "name" { 378 | tc.name = val 379 | } 380 | 381 | if key == "time" { 382 | t, err := dns.StringToTime(val) 383 | if err != nil { 384 | panic("unable to parse time used in test: " + err.Error()) 385 | } 386 | 387 | tc.time = time.Unix(int64(t), 0) 388 | } 389 | }) 390 | continue 391 | case strings.HasPrefix(line, "[TEST_END]"): 392 | begin = false 393 | t.Run(th.zone+":"+tc.name, func(t *testing.T) { 394 | run(t, &th, &tc) 395 | }) 396 | tc = testCase{} 397 | continue 398 | case strings.HasPrefix(line, "[RESULT]"): 399 | parseKeyValPairs(line[8:], ",", func(key string, val string) { 400 | switch key { 401 | case "secure": 402 | tc.secure = val == "1" 403 | case "bogus": 404 | tc.bogus = val == "1" 405 | } 406 | }) 407 | continue 408 | } 409 | 410 | switch scanning { 411 | case 1: 412 | th.anchors += line + "\n" 413 | case 2: 414 | th.dnsKeys += line + "\n" 415 | case 3: 416 | tc.inputMsg += line + "\n" 417 | case 4: 418 | tc.filteredMsg += line + "\n" 419 | } 420 | } 421 | } 422 | 423 | func zoneToRecords(z string) []dns.RR { 424 | var records []dns.RR 425 | tokens := dns.NewZoneParser(strings.NewReader(z), "", "") 426 | for x, ok := tokens.Next(); ok; x, ok = tokens.Next() { 427 | err := tokens.Err() 428 | if err != nil { 429 | panic(err) 430 | } 431 | records = append(records, x) 432 | } 433 | return records 434 | } 435 | 436 | func recordsToZone(rrs []dns.RR) string { 437 | var b strings.Builder 438 | for _, rr := range rrs { 439 | b.WriteString(rr.String() + "\n") 440 | } 441 | return b.String() 442 | } 443 | 444 | func recordsToZoneSorted(rrs []dns.RR) string { 445 | sort.Slice(rrs, func(i, j int) bool { 446 | return rrs[i].String() < rrs[j].String() 447 | }) 448 | 449 | return recordsToZone(rrs) 450 | } 451 | 452 | func stringToMsg(str string) (*dns.Msg, error) { 453 | msg := new(dns.Msg) 454 | sc := bufio.NewScanner(strings.NewReader(str)) 455 | sectionId := -2 456 | 457 | for sc.Scan() { 458 | line := strings.TrimSpace(sc.Text()) 459 | if strings.HasPrefix(line, ";") { 460 | line := strings.TrimFunc(line, func(r rune) bool { 461 | if r == ';' || r == ' ' || r == '\t' { 462 | return true 463 | } 464 | return false 465 | }) 466 | 467 | switch { 468 | case strings.HasPrefix(line, "->>HEADER<<-"): 469 | line := strings.TrimSpace(line[12:]) 470 | err := parseKeyValPairs(line, ",", func(key string, val string) { 471 | switch key { 472 | case "opcode": 473 | msg.Opcode = dns.StringToOpcode[val] 474 | case "status": 475 | msg.Rcode = dns.StringToRcode[val] 476 | } 477 | }) 478 | if err != nil { 479 | return nil, err 480 | } 481 | case strings.HasPrefix(line, "flags"): 482 | err := parseKeyValPairs(line, ";", func(key string, val string) { 483 | switch key { 484 | case "flags": 485 | flags := strings.Split(val, " ") 486 | for _, flag := range flags { 487 | flag = strings.TrimSpace(flag) 488 | switch flag { 489 | case "qr": 490 | msg.Response = true 491 | case "rd": 492 | msg.RecursionDesired = true 493 | case "ra": 494 | msg.RecursionAvailable = true 495 | case "ad": 496 | msg.AuthenticatedData = true 497 | case "cd": 498 | msg.CheckingDisabled = true 499 | case "aa": 500 | msg.AuthenticatedData = true 501 | case "tc": 502 | msg.Truncated = true 503 | } 504 | } 505 | } 506 | }) 507 | if err != nil { 508 | return nil, err 509 | } 510 | case strings.Contains(line, "flags: do"): 511 | msg.SetEdns0(4096, true) 512 | case line == "QUESTION SECTION:": 513 | sectionId = -1 514 | case line == "ANSWER SECTION:": 515 | sectionId = 0 516 | case line == "AUTHORITY SECTION:": 517 | sectionId = 1 518 | case line == "ADDITIONAL SECTION:": 519 | sectionId = 2 520 | case sectionId == -1: 521 | parts := strings.Fields(line) 522 | if len(parts) != 3 { 523 | return nil, errors.New("bad question") 524 | } 525 | 526 | msg.Question = make([]dns.Question, 1) 527 | msg.Question[0] = dns.Question{ 528 | Name: parts[0], 529 | Qtype: dns.StringToType[parts[2]], 530 | Qclass: dns.StringToClass[parts[1]], 531 | } 532 | } 533 | continue 534 | } 535 | 536 | if line == "" { 537 | continue 538 | } 539 | 540 | switch sectionId { 541 | case 0: 542 | rr, err := dns.NewRR(line) 543 | if err != nil { 544 | return nil, err 545 | } 546 | msg.Answer = append(msg.Answer, rr) 547 | case 1: 548 | rr, err := dns.NewRR(line) 549 | if err != nil { 550 | return nil, err 551 | } 552 | msg.Ns = append(msg.Ns, rr) 553 | case 2: 554 | rr, err := dns.NewRR(line) 555 | if err != nil { 556 | return nil, err 557 | } 558 | msg.Extra = append(msg.Extra, rr) 559 | } 560 | } 561 | 562 | return msg, nil 563 | } 564 | 565 | func parseKeyValPairs(line string, sep string, onRead func(key string, val string)) error { 566 | parts := strings.Split(line, sep) 567 | for _, part := range parts { 568 | keyVal := strings.Split(part, ":") 569 | if len(keyVal) < 2 { 570 | return errors.New("bad key value pair") 571 | } 572 | key := strings.TrimSpace(keyVal[0]) 573 | val := strings.TrimSpace(keyVal[1]) 574 | 575 | if len(keyVal) > 2 { 576 | key = "" 577 | val = strings.Join(keyVal, ":") 578 | } 579 | 580 | onRead(key, val) 581 | } 582 | return nil 583 | } 584 | 585 | func dsToRR(dsSet []*dns.DS) []dns.RR { 586 | var rrs []dns.RR 587 | for _, rr := range dsSet { 588 | rrs = append(rrs, rr) 589 | } 590 | return rrs 591 | } 592 | -------------------------------------------------------------------------------- /internal/resolvers/dnssec/testdata/val_hns.txt: -------------------------------------------------------------------------------- 1 | [ZONE] origin: ., time: 20210823000000 2 | [TRUST_ANCHORS] 3 | . 10800 IN DS 35215 13 2 7C50EA94A63AEECB65B510D1EAC1846C973A89D4AB292287D5A4D715 136B57A3 4 | 5 | [DNSKEYS] 6 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 5107 7 | ;; flags: qr aa rd; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1 8 | ;; WARNING: recursion requested but not available 9 | 10 | ;; OPT PSEUDOSECTION: 11 | ; EDNS: version: 0, flags: do; udp: 4096 12 | ;; QUESTION SECTION: 13 | ;. IN DNSKEY 14 | 15 | ;; ANSWER SECTION: 16 | . 10800 IN DNSKEY 257 3 13 T9cURJ2M/Mz9q6UsZNY+Ospyvj+Uv+tgrrWkLtPQwgU/Xu5Yk0l02Sn5 ua2xAQfEYIzRO6v5iA+BejMeEwNP4Q== 17 | . 10800 IN DNSKEY 256 3 13 I5nPs6clFa1gnwn9IpVDGdJLfEONzgD1NcfuEwEIVuIoHdZGgvVblsLN bRO+spW3nQYHg92svhy1HOjTiFBIsQ== 18 | . 10800 IN RRSIG DNSKEY 13 0 10800 20210903071710 20210806071710 35215 . di4uA/VccVv3H6syAt8aoqk2qjfAsvmKR4fyNqe+mrfkOSuXfc6kauqZ K/37ikNjWNUcm/MMzO4n7IlWxdlFfA== 19 | 20 | [TEST_BEGIN] name: verify referral 21 | [INPUT] 22 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 60811 23 | ;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 3 24 | ;; WARNING: recursion requested but not available 25 | 26 | ;; OPT PSEUDOSECTION: 27 | ; EDNS: version: 0, flags: do; udp: 4096 28 | ;; QUESTION SECTION: 29 | ;omnitude. IN DNSKEY 30 | 31 | ;; AUTHORITY SECTION: 32 | omnitude. 21600 IN NS _5l6tm80._synth. 33 | omnitude. 21600 IN NS _400hjs000l2gol000fvvsc9cpg._synth. 34 | omnitude. 21600 IN DS 26614 8 2 A20BC6F9ADA0883326A05374D0D0C0E6290CEF580D00B9B957703014 41733B7F 35 | omnitude. 10800 IN RRSIG DS 13 1 21600 20210903072110 20210806072110 60944 . smOZAe0yrZakzqaxIKN27WDTGXb2ld4BfYPbR+0TIQo4GSrnIxuQ5cOa fLp5DModWvSNtRoVj189g8H4pfk4yQ== 36 | 37 | ;; ADDITIONAL SECTION: 38 | _5l6tm80._synth. 21600 IN A 45.77.219.32 39 | _400hjs000l2gol000fvvsc9cpg._synth. 21600 IN AAAA 2001:19f0:5:450c:5400:3ff:fe31:2ccc 40 | 41 | [RESULT] secure: 1, bogus: 0 42 | [TEST_END] 43 | 44 | [ZONE] origin: omnitude. 45 | [TRUST_ANCHORS] 46 | omnitude. 21600 IN DS 26614 8 2 A20BC6F9ADA0883326A05374D0D0C0E6290CEF580D00B9B957703014 41733B7F 47 | 48 | [DNSKEYS] 49 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 25408 50 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1 51 | 52 | ;; OPT PSEUDOSECTION: 53 | ; EDNS: version: 0, flags: do; udp: 4096 54 | ;; QUESTION SECTION: 55 | ;omnitude. IN DNSKEY 56 | 57 | ;; ANSWER SECTION: 58 | omnitude. 299 IN DNSKEY 256 3 8 AwEAAdzaxiS3FYhvytBxmOBTUy9SL8LEyoTKvaPI7RjJ1jxM2jjd1ncJ +ZQ4BGvrId8ptfqprxTpw6C+s0O9qZ2DBsHo2lubYI3EQkMfOOVLN5my HUjFSUjb9u1+Vs5BJ6bFtUvbR9GMiy9Go4x59gx6MgM+iIICMxJx42vW X7+fBXUJDhTEhKelP/LPCJoPO1jPsX/8YFU7Y+9rjNpIfLb775F9iCW7 hGn8Vjk05x4moQWctG9pQWXWk58pVXQfwoLv31E1kE4sW2MM9iroVPu8 Ey9+NRjdWDPrqcvS6eW+oBfe7KgzhoBqhqbYya/Xfc8duSUZ21OsMZke dweffXR/T1k= 59 | omnitude. 299 IN DNSKEY 257 3 8 AwEAAbBGvxY7WeAzD/4PVb7ZAIVRCv/gdHlKNDw4rpBfk1ed0hXKpXcX S4Es6QhgaZ9cvWT5YjS+BFSL1pkTM/DU3GQP55T1FHKQQTk9o++1A1Z2 iUzijIRy/1rjqD7jLmw5L2ZfoCIBxFhmyyOJ26oVfbZlwyrBTueVkmiv U6PGf1c61vP/XE3hq7xP5BumabvsUTl5US/fqcFTatpcjQIX868k5c+E jHpE6zts5s1XF+l0r2UMjZJbG1GQKGSPfBNU9hR5UjxoSs9zxgThqDBn WpHNBQxn/K5f8cVXq93UgG//gwL3DRoewdx/CckqMkQ87btAprEHYCqu boEzwTaKuLcJzEmpEhxIm3C2o/BId3ITqpRgQckOeGMxa6C87EsO0LdF Mh0kTugMfwsvgTZLNzz7uuE52a9pP1tipL2YkC8wApQ33egd6j/EXK6G tNV9vmADdYqEeXPtLHsuDHCl4oCOOkDd2ykzrXnF0k84yqbRIVOEWfMv F2/6UIqtesjJL3E577QXIUvkGOLi+FLt5KUfRDSbN8pJjBilbiH6eDBB hhN4FnJMQsjiAPmlXlk+jrqEgICZi22yaJijIiUPMluHEfnOFHOC+QeG l1QPuF1h70YbiVl/f3ukV7ovxtg+qj/2bqMUPhOJ60+s6bIx0RHvt6GF x+3DUtAZmXyt8py3 60 | omnitude. 299 IN RRSIG DNSKEY 8 1 300 20220801000000 20210730150002 26614 omnitude. bEnEn2sB/0KPfvoibG+qIlu3dOFqj0/+obaD/kzh6yiP3jHpwnktmEid 7G2K2zHNhL8KMwwJJBr5dW2fMTwt20qOvLdzP634zu1h1UiFi6h4Nln2 l1qRv4WYV8EvFl4SA+NTzDPyCHORt6CGIjTwTiMBKxX5fAX/FuVFOfyg eyGIpwZPhYHZSB35v7lpe9ifTcHEX8vYd4doHP/Q8KQ2D3hfPQIiZz4m Nd0n7co4pGRKlL6u68J+be7Z0ijtfmjk9+oVnwOuVwEdHztyPtxFOGQW kij+VVi0/fsIgHrZ2VwJ2lC3Iu1JMHK9nChl80WqfoM4ios8kZodXIKh QynFs93dp13zgVjDN52SxtDrfkxMyjpaGNmjbZ5a5Ykfqgb4xzHfyzyx WA6jPIYxTUPrWJPYcTMSExMk+1gBOYhljaBXF3FjfURyKIiLcvQGux57 gMv1+HewcJ67oX10K7O4bTwa4UVf+vTb8TmT/tS2Q/aBpFsOg2Q52plp QAs5NgGaR9cNgagA4MMyrdytmV2hlZ6qP4/8G9mcMDNZWMVHMQXvBRhM J9EoyVFzz+h0neGYQSjvVHYYXJdvAptX39vbXbv4DC4yHeC3QDUvtfv0 tFf+XQLy5FwGUIgmPImtQKJdSyB6SpT7dLU8YZnae12o8Sr548YBhV6v LrslSRitNio= 61 | 62 | 63 | [TEST_BEGIN] name: verify answer 64 | [INPUT] 65 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 34114 66 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 67 | 68 | ;; OPT PSEUDOSECTION: 69 | ; EDNS: version: 0, flags: do; udp: 4096 70 | ;; QUESTION SECTION: 71 | ;omnitude. IN A 72 | 73 | ;; ANSWER SECTION: 74 | omnitude. 300 IN A 45.77.219.32 75 | omnitude. 300 IN RRSIG A 8 1 300 20220801000000 20210730150002 53619 omnitude. XEf2n745MG93mpIYa1LvXIAIMmXUDdppiIWDUxnF8vK7V1xD07GduO0w ii2ATE7ltuGcjXNyT6r4snGXDuQWZKJz+j9K5pLM0hiIJIMM7CdOb5pJ ZCjfRqiZb7Fl7TVLry3y1zINJ/VcNJYXgQiCOvwYkC7HmCBLg1F0jZjx PKI+iKYhyq+d/XF3R8Vgt5X1jEh5KuwcPZyiDT2JceaEWoXeY5F6OQso gwYrdXUYJVwzAlBs28bC9GEi5f3z45EaErPyomlJOMtth+Q9JnEPgOO9 wsZ3RrM/dybz5s0OeF2njPxKMGFukjuIC2ReTbfL3oqiVe7RdlQNkRfV LARdrQ== 76 | 77 | [RESULT] secure: 1, bogus: 0 78 | [TEST_END] 79 | 80 | [TEST_BEGIN] name: downgrade nsec3 81 | [INPUT] 82 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 7516 83 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1 84 | 85 | ;; OPT PSEUDOSECTION: 86 | ; EDNS: version: 0, flags: do; udp: 4096 87 | ;; QUESTION SECTION: 88 | ;omnitude. IN TLSA 89 | 90 | ;; AUTHORITY SECTION: 91 | omnitude. 300 IN SOA omnitude. admin.omnitude. 2021020910 3600 1800 604800 3600 92 | omnitude. 300 IN RRSIG SOA 8 1 300 20220801000000 20210730150002 53619 omnitude. KEPg/qbZdHaEaJqdDyKwMkmXxCCpJj0WRlvnV7prHTVJ47meCEc2C8Qu Tpiut8q9V/tGtokT+zYEiEl0mqhd2DeIWQMKNwifk+zHw//31TnOe3Tl VM4Kp9GKE/8xczvOaTAxx6p/Fsu8WBkf6jbQ025rh1BCdYWukIQT6mKz 1zIluwqfmfZuz1V5FaIu6w4REMv+7F6OBbYgO8HhMgOpbgd6/368abU3 hin+Af7BCfe1oTnWvrWoLnEfwjpJDJqhJ81KBhSsOu8RHLgqPOb2M5mc g1JhQ5Jp3JQMfhifc2ssj3upqlxSdnRg8IY4WLN8k2PC9+5onsai28Ff UAJl+g== 93 | 7o2vmqoh0fpu4vuhe481p915l94m9iq4.omnitude. 3600 IN NSEC3 1 0 1 3C628D8438ED4024 9344CILQB9599PT01FB97JTEKIQOVJ9R A NS SOA AAAA RRSIG DNSKEY NSEC3PARAM 94 | 7o2vmqoh0fpu4vuhe481p915l94m9iq4.omnitude. 3600 IN RRSIG NSEC3 8 2 3600 20220801000000 20210730150002 53619 omnitude. wpdmBm9Mt11YUz4kv3mCfLk3bW9/JqFCj0f74xfaCRZCWnYRxw/NaQTm A/VSSl1uMEsogxBXcxJrO9b9OXbv8KfmjPow5oVsZc9vm8WWK3riGpzy 26fQhdaevZoemWGRY1U8p2OvF5Ki+7DgwzmFf1Q+XIfjm6bdG5DwQhI2 ulin1TwpGKg+0PUceviiD4ADWTwH5Y+op/wzozvqw6L37+5CH4/5QUNz 8IzAziT8nPSCSHW+Jx0BdNW3bBFF5KNyfiCif+B4cOMqI8DXmw9YcXxh Pk9lMQ/5B9y9Am5e4y7BeLmBghDwNvDUXU9ilrMCy9unEIBWs/2NDAxY sTCvvg== 95 | 96 | [RESULT] secure: 0, bogus: 0 97 | [TEST_END] 98 | 99 | 100 | [ZONE] origin: proofofconcept., time: 20210823000000 101 | [TRUST_ANCHORS] 102 | proofofconcept. 21551 IN DS 17552 8 2 BBBE70AF5CD965360442CBEF40E3299344AF493D339592B93DAA29F7 839C1D58 103 | 104 | [DNSKEYS] 105 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 59719 106 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1 107 | 108 | ;; OPT PSEUDOSECTION: 109 | ; EDNS: version: 0, flags: do; udp: 4096 110 | ;; QUESTION SECTION: 111 | ;proofofconcept. IN DNSKEY 112 | 113 | ;; ANSWER SECTION: 114 | proofofconcept. 86398 IN DNSKEY 256 3 8 AwEAAbvBTKZrkStI4JnYDKPUiAM0i1ZCebo0y/dGGviZDxyJdeA6sfaR camxoq6emggevLyD32YBURhdxT+DjBWMyYfLf80vqytXLqZFPbdLRSK7 csqPaaIFuQbvRFBECPU40NvRqVxq8AMb5NenlyFHUCq7hSUIJm9sisQ2 9f4QuvDeyv787StrjRsohLzWwzjJmWCsa1oafT3RUDL09PEc1EZ/OZMN UBg2k0I+/S83rYlqHhv4qoQnmnXLsZy8wGxM74ABjySW5gba+zSUMzY5 AoRMfXP/S6CXNx0zHBC5uF+8CHijoZ0gOKPXYhBmSYKFYov7qatq3KGH zfGYXArnoJs= 115 | proofofconcept. 86398 IN DNSKEY 257 3 8 AwEAAbCsgdJOX7TxLidghkJEP17JM/F5kPrbpwozl5/Onp95vgx33bRO px8lhtIRevLBYpaG8rPiLjpvAnwdrkdLP+Mz72gly6eO1INwhoEzMcPl CS6IfRk8BOBVcA2mv+DhtMmIpGjZaoLZt9S0Rf9KfAJKWIA+aZ5vxU1C u9vyB3rnC5Ia7idCiBjVoa+6UXHUnkuuWGyMAp0L7fbelpPkEfdWkLHh IfruMTgbexrUyderee1+DEghDJHV9Eax4bcjEIcsM7qkbmjo1l1c7tS6 VZdZD7vWL8OhuWCa/xha2bDLa5bfc4jZtVs18qqRIPN1klQOfvSGBRkw IEGZNY8QXCE= 116 | proofofconcept. 86398 IN RRSIG DNSKEY 8 1 172800 20220225162619 20210224162619 17552 proofofconcept. ciJQmxWaTVD2KSh7CmDhFkS1aAOMXh/Rz7clilxGbP/TSZzX/mYArrcs 1CZyStB89DX1CgEKf05enHSadg3wHnUQg1CPJ2Au2GBTS7C4bMftjGH/ XTyXGAbEWJcA/5aI9dg7KBG0GSedx1fEH4YPPTEScnHzCpqQouv4LYGS cbZwIXKvPw0kAtRbsh+hopAELzQqh/NU/d6MUNimGUn+7D7Z8C6k3bN2 8p2AT5o1GqDVxYSS27v/bxy41rKMfxU4Aduq+NtjmV7XSUA73fpmf8Yz sDQDvCXuzK11AXxTRdGzyMfH/V7wClgtU4TG+GGfBViPt1E9BA+Xa1Br ecEBjg== 117 | proofofconcept. 86398 IN RRSIG DNSKEY 8 1 172800 20220225162619 20210224162619 58608 proofofconcept. AUS2qkXJpc0WeC9ctZz4Rts3dMXq7Z376TGvLcFLBbI3T65i4T3cp365 pKzbtRgpHCVWPhqXsFY9wdy0XPoEREhLMc6mNkt9G4UE44Lr3vTkptNx qBNtJiVwX51oFXxok8IIZU8GWFdPg6rUz4jr4TZEdJ5951KLEvpCRQcz r2UIckIxvl5RfEz3Y9EvBVsLwNVaOSzlozdekKWSQxR8ZvnsQZA9UypN io5OCSoeidWftOvroWmjTCPD5550bzGN+U8Ug84GCB3rIF83JbyNqsUd PA/H2lVMFaEOHEG/YLCYB9+A+EVtHqBkhWiaPVcwRXfylcPbaTykMgXy DXzkrQ== 118 | 119 | [TEST_BEGIN] name: verify answer 120 | [INPUT] 121 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 27762 122 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 123 | 124 | ;; OPT PSEUDOSECTION: 125 | ; EDNS: version: 0, flags: do; udp: 4096 126 | ;; QUESTION SECTION: 127 | ;proofofconcept. IN A 128 | 129 | ;; ANSWER SECTION: 130 | proofofconcept. 21600 IN A 142.93.115.133 131 | proofofconcept. 86400 IN RRSIG A 8 1 21600 20220225162619 20210224162619 58608 proofofconcept. WDp91leX1YhEHkRmypxSg1yC8vGXHonCfXDLRTxePKDAXzvrbPs8rF6I jT1Db5G97+Deb9wJ9UARV73+0u+EePm/T/YQ4v907bCvdCVBSphmmWsi znDi/ZFoOmYVQPnmPyvT2A75PhUCwDW907U0c+NUcKCR1te/dmxrHOcm bKsYRj35Pc+wt2belMFqKArpqVtRh5v2sYUcbkBLJx57ng7JyHl1B+PH 0eTXfyRL41pmcsRUrYkvcqPyeNaT7GqCkWM2ADpC8JzXzCXIt4gMm6VG jhdfyTg1N9WaEJvFvn+JD7+U4oeas4athsbF9NkdBAY1lOIxSwiAEvsy uKMUaw== 132 | [RESULT] secure: 1, bogus: 0 133 | [TEST_END] 134 | 135 | [TEST_BEGIN] name: broken nodata response missing nsec 136 | [INPUT] 137 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 16484 138 | ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 2, ADDITIONAL: 1 139 | 140 | ;; OPT PSEUDOSECTION: 141 | ; EDNS: version: 0, flags: do; udp: 4096 142 | ;; QUESTION SECTION: 143 | ;proofofconcept. IN AAAA 144 | 145 | ;; AUTHORITY SECTION: 146 | proofofconcept. 156 IN SOA ns.proofofconcept. email.proofofconcept. 1614270379 86400 7200 604800 300 147 | proofofconcept. 86256 IN RRSIG SOA 8 1 21600 20220225162619 20210224162619 58608 proofofconcept. KhkbxVkXJQUO8PxO9DvcG7clgS4lcMgKtOe2j/UnwBl3o47UC0KleMtN Vpu9Aa/pFPurh1qE1n6KHocnqyyUWje9OtvGs8bL22ybU7ookBJJqplW SBMnxRnDcNr3ygNsBPZZOYpVMpD9z/Kxz1cMLSntvCYXlJCrHA8HGp52 ZOwoZk1ed4lzr87uSvnmnhUa180OnQcSfAUtkUToVo0bleDL8CscNhPy LqV12CBOF4IjYRln3LQZzV4T8wOkv6xNHQAUClijQ/x+ny9aZF+Suspc Ulrg2WA7Wys8war7DanZnhbgxGoVK5rmfNgftXlk/sqpFOvczviWWkc2 NTLVUw== 148 | [RESULT] secure: 0, bogus: 1 149 | [TEST_END] 150 | 151 | 152 | [TEST_BEGIN] name: verify answer subdomains 153 | [INPUT] 154 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 16378 155 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 156 | 157 | ;; OPT PSEUDOSECTION: 158 | ; EDNS: version: 0, flags: do; udp: 4096 159 | ;; QUESTION SECTION: 160 | ;_443._tcp.proofofconcept. IN TLSA 161 | 162 | ;; ANSWER SECTION: 163 | _443._tcp.proofofconcept. 3600 IN TLSA 3 1 1 22E3C95A736E370FA38E7D94239F49C7A9EF961AF94E05AE8CC74FC3 CA2BA5CE 164 | _443._tcp.proofofconcept. 86400 IN RRSIG TLSA 8 3 3600 20220225162619 20210224162619 58608 proofofconcept. gCIZIIm6KkUDDee4kqmuEjZ9hhTUPgoORUKwgTrzlIVV0p+4B83duHWV S/amZfpNwM0vWJ25If9UstG/q6QI1PrbJuSoonKl9eXaAihbZ1csfV4Q 7hC2JyrlarAsv7VjvnQwc31DYxywwOCfLuV5aeo1CFLrtsOGgnG6eJXp MXb79351FqBS2xVUvNyZfMLFcVq5xgAenH3JP5KwuOsBgNw+1mtLBg8N DQVQaIlPz/9/l9wrDqhuzhciOhFkHHZcq/cD/W1xsEgTJow3WdaIbAs1 FTy7CwowPeiYLD9RCnTYMv66cdCEvqKUgIHa2ib9k9d0D8zmoQNhIxHJ H9fSDQ== 165 | [RESULT] secure: 1, bogus: 0 166 | [TEST_END] 167 | 168 | 169 | 170 | [ZONE] origin: letsdane., time: 20210824000000 171 | [TRUST_ANCHORS] 172 | letsdane. 21591 IN DS 28057 15 2 BFF60097255A21E8054EB53D74481B4AA6E51C1B85F8BCE97A6CF5AE C91ECBB0 173 | 174 | [DNSKEYS] 175 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 6266 176 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1 177 | 178 | ;; OPT PSEUDOSECTION: 179 | ; EDNS: version: 0, flags: do; udp: 4096 180 | ;; QUESTION SECTION: 181 | ;letsdane. IN DNSKEY 182 | 183 | ;; ANSWER SECTION: 184 | letsdane. 3565 IN DNSKEY 256 3 15 sHSzJPaSZ/KzG+tArxLITJxZgv1bqwVcUA6/kr+hPsM= 185 | letsdane. 3565 IN DNSKEY 257 3 15 FeDD+6E44LAYo8sJtpzfbyLOkCMKePxArIEK0OxkNqk= 186 | letsdane. 3565 IN RRSIG DNSKEY 15 1 3600 20210828070441 20210820040441 28057 letsdane. 0fbi69t237RgF0HAD2LwHkdh+AQJ3bCRkn23mrXluZn0M7vEs11TDJIr ahnuBYWZ4gQvyRB+pmReHgQlXE0wBA== 187 | 188 | 189 | [TEST_BEGIN] name: verify answer ed25519 190 | [INPUT] 191 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 37808 192 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 193 | 194 | ;; OPT PSEUDOSECTION: 195 | ; EDNS: version: 0, flags: do; udp: 4096 196 | ;; QUESTION SECTION: 197 | ;letsdane. IN A 198 | 199 | ;; ANSWER SECTION: 200 | letsdane. 230 IN A 157.230.75.71 201 | letsdane. 230 IN RRSIG A 15 1 3600 20210828070441 20210820040441 27214 letsdane. x/XLBa5Yg4L13mqIUGL/r3LeQhXb5zJMW8c7ipVkuef2EuKt3YFDXlRV 6gp02ekW0uvmaUYHqfS5FI1fTdtlAQ== 202 | 203 | [RESULT] secure: 1, bogus: 0 204 | [TEST_END] 205 | 206 | 207 | [TEST_BEGIN] name: nodata response ed25519/black lies 208 | [INPUT] 209 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 36979 210 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1 211 | 212 | ;; OPT PSEUDOSECTION: 213 | ; EDNS: version: 0, flags: do; udp: 4096 214 | ;; QUESTION SECTION: 215 | ;a.letsdane. IN A 216 | 217 | ;; AUTHORITY SECTION: 218 | letsDAne. 162 IN SOA ns1.buffrr.dev. contact.buffrr.dev. 2 10000 2400 604800 300 219 | letsdanE. 162 IN RRSIG SOA 15 1 3600 20210828070441 20210820040441 27214 letsdane. z3LDh+xRAdQnM5s2ZLUUZ7uYTSO9MhtvbwGbIwhW7EeRaSGOV+YW8JP+ JsVd0dsQEGZS2tplAKpzfuQ+suy1BQ== 220 | a.letsdane. 300 IN NSEC \000.a.letsdane. HINFO TXT AAAA LOC SRV CERT SSHFP RRSIG NSEC TLSA HIP OPENPGPKEY SPF 221 | a.letsdane. 300 IN RRSIG NSEC 15 2 3600 20210828070659 20210820040659 27214 letsdane. Qy3X9rIL8oeUh0JurSyc22EsQKGagBAh3+DidzgPplUprV2chDo3s8r5 ec0jA32gMVXpt6NqkJ4RQpomRq4jAA== 222 | 223 | [RESULT] secure: 1, bogus: 0 224 | [TEST_END] -------------------------------------------------------------------------------- /internal/resolvers/dnssec/dnssec.go: -------------------------------------------------------------------------------- 1 | package dnssec 2 | 3 | // dnssec validation utilities loosely based on 4 | // https://gitlab.nic.cz/knot/knot-resolver/-/tree/master/lib/dnssec 5 | // https://github.com/semihalev/sdns 6 | 7 | import ( 8 | "bytes" 9 | "crypto/rsa" 10 | "encoding/base64" 11 | "errors" 12 | "fmt" 13 | "github.com/miekg/dns" 14 | "math/big" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | var ( 20 | ErrNoDNSKEY = errors.New("no valid dnskey records found") 21 | ErrBadDS = errors.New("DS record doesn't match zone name") 22 | ErrNoSignatures = errors.New("no rrsig records for zone that should be signed") 23 | ErrMissingDNSKEY = errors.New("no matching dnskey found for rrsig records") 24 | ErrSignatureBailiwick = errors.New("rrsig record out of bailiwick") 25 | ErrInvalidSignaturePeriod = errors.New("incorrect signature validity period") 26 | ErrMissingSigned = errors.New("signed records are missing") 27 | ) 28 | 29 | // supported dnssec algorithms weaker/unsupported algorithms are treated as unsigned 30 | var supportedAlgorithms = []uint8{dns.RSASHA256, dns.RSASHA512, dns.ECDSAP256SHA256, dns.ECDSAP384SHA384, dns.ED25519} 31 | var supportedDigests = []uint8{dns.SHA256, dns.SHA384} 32 | 33 | // DefaultMinRSAKeySize the minimum RSA key size 34 | // that can be used to securely verify messages 35 | const DefaultMinRSAKeySize = 2048 36 | 37 | func filterDS(zone string, dsSet []dns.RR) ([]*dns.DS, error) { 38 | if !dns.IsFqdn(zone) { 39 | return nil, fmt.Errorf("zone must be fqdn") 40 | } 41 | 42 | type dsKey struct { 43 | keyTag uint16 44 | algorithm uint8 45 | } 46 | 47 | supported := make(map[dsKey]*dns.DS) 48 | for _, rr := range dsSet { 49 | if !strings.EqualFold(zone, rr.Header().Name) { 50 | return nil, ErrBadDS 51 | } 52 | 53 | ds, ok := rr.(*dns.DS) 54 | if !ok { 55 | continue 56 | } 57 | 58 | if !isAlgorithmSupported(ds.Algorithm) || 59 | !isDigestSupported(ds.DigestType) { 60 | continue 61 | } 62 | 63 | key := dsKey{ 64 | keyTag: ds.KeyTag, 65 | algorithm: ds.Algorithm, 66 | } 67 | 68 | // pick strongest supported digest type 69 | if ds2, ok := supported[key]; ok { 70 | if ds2.DigestType >= ds.DigestType { 71 | continue 72 | } 73 | } 74 | 75 | supported[key] = ds 76 | } 77 | 78 | var values []*dns.DS 79 | for _, rr := range supported { 80 | values = append(values, rr) 81 | } 82 | 83 | return values, nil 84 | } 85 | 86 | func fromBase64(s []byte) (buf []byte, err error) { 87 | buflen := base64.StdEncoding.DecodedLen(len(s)) 88 | buf = make([]byte, buflen) 89 | n, err := base64.StdEncoding.Decode(buf, s) 90 | buf = buf[:n] 91 | return 92 | } 93 | 94 | func shouldDowngradeKey(k *dns.DNSKEY, minKeySize int) bool { 95 | if k.Algorithm != dns.RSASHA512 && k.Algorithm != dns.RSASHA256 { 96 | return false 97 | } 98 | 99 | // extracted from miekg/dns to check if 100 | // an exponent is supported by the crypto package 101 | keybuf, err := fromBase64([]byte(k.PublicKey)) 102 | if err != nil { 103 | return false 104 | } 105 | 106 | if len(keybuf) < 1+1+64 { 107 | // Exponent must be at least 1 byte and modulus at least 64 108 | return false 109 | } 110 | 111 | // RFC 2537/3110, section 2. RSA Public KEY Resource Records 112 | // Length is in the 0th byte, unless its zero, then it 113 | // it in bytes 1 and 2 and its a 16 bit number 114 | explen := uint16(keybuf[0]) 115 | keyoff := 1 116 | if explen == 0 { 117 | explen = uint16(keybuf[1])<<8 | uint16(keybuf[2]) 118 | keyoff = 3 119 | } 120 | 121 | if explen > 4 { 122 | // Exponent larger than supported by the crypto package 123 | return true 124 | } 125 | 126 | if explen == 0 || keybuf[keyoff] == 0 { 127 | // Exponent empty, or contains prohibited leading zero. 128 | return false 129 | } 130 | 131 | modoff := keyoff + int(explen) 132 | modlen := len(keybuf) - modoff 133 | if modlen < 64 || modlen > 512 || keybuf[modoff] == 0 { 134 | // Modulus is too small, large, or contains prohibited leading zero. 135 | return false 136 | } 137 | 138 | pubkey := new(rsa.PublicKey) 139 | 140 | var expo uint64 141 | // The exponent of length explen is between keyoff and modoff. 142 | for _, v := range keybuf[keyoff:modoff] { 143 | expo <<= 8 144 | expo |= uint64(v) 145 | } 146 | if expo > 1<<31-1 { 147 | // Larger exponent than supported by the crypto package. 148 | return true 149 | } 150 | 151 | pubkey.E = int(expo) 152 | pubkey.N = new(big.Int).SetBytes(keybuf[modoff:]) 153 | 154 | // downgrade if using a weak key size 155 | if pubkey.N.BitLen() < minKeySize { 156 | return true 157 | } 158 | 159 | return false 160 | } 161 | 162 | func isAlgorithmSupported(algo uint8) bool { 163 | for _, curr := range supportedAlgorithms { 164 | if algo == curr { 165 | return true 166 | } 167 | } 168 | 169 | return false 170 | } 171 | 172 | func isDigestSupported(digest uint8) bool { 173 | for _, curr := range supportedDigests { 174 | if digest == curr { 175 | return true 176 | } 177 | } 178 | 179 | return false 180 | } 181 | 182 | func VerifyDNSKeys(zone string, msg *dns.Msg, parentDSSet []dns.RR, t time.Time, minKeySize int) (map[uint16]*dns.DNSKEY, error) { 183 | var err error 184 | var dsSet []*dns.DS 185 | 186 | if dsSet, err = filterDS(zone, parentDSSet); err != nil { 187 | return nil, err 188 | } 189 | 190 | if len(dsSet) == 0 { 191 | return nil, nil 192 | } 193 | 194 | matchingKeys := make(map[uint16]*dns.DNSKEY) 195 | 196 | for _, ds := range dsSet { 197 | for _, rr := range msg.Answer { 198 | if rr.Header().Rrtype != dns.TypeDNSKEY { 199 | continue 200 | } 201 | 202 | // simple checks 203 | key := rr.(*dns.DNSKEY) 204 | if key.Protocol != 3 { 205 | continue 206 | } 207 | if key.Flags != 256 && key.Flags != 257 { 208 | continue 209 | } 210 | if key.Algorithm != ds.Algorithm { 211 | continue 212 | } 213 | 214 | tag := key.KeyTag() 215 | if tag != ds.KeyTag { 216 | continue 217 | } 218 | 219 | dsFromKey := key.ToDS(ds.DigestType) 220 | if dsFromKey == nil { 221 | continue 222 | } 223 | 224 | if !strings.EqualFold(dsFromKey.Digest, ds.Digest) { 225 | continue 226 | } 227 | 228 | // we have a valid key 229 | matchingKeys[tag] = key 230 | 231 | } 232 | } 233 | 234 | if len(matchingKeys) == 0 { 235 | return nil, ErrNoDNSKEY 236 | } 237 | 238 | validKeys := make(map[uint16]*dns.DNSKEY) 239 | 240 | for _, key := range matchingKeys { 241 | if !shouldDowngradeKey(key, minKeySize) { 242 | validKeys[key.KeyTag()] = key 243 | } 244 | } 245 | 246 | if len(validKeys) == 0 { 247 | return nil, nil 248 | } 249 | 250 | // verifySignatures will clean up the answer 251 | // section in the msg with only the valid rr sets 252 | secure, err := verifySignatures(zone, zone, msg, validKeys, t, minKeySize) 253 | if err != nil { 254 | return nil, err 255 | } 256 | 257 | if !secure { 258 | return nil, nil 259 | } 260 | 261 | if len(msg.Answer) == 0 { 262 | return nil, ErrNoDNSKEY 263 | } 264 | 265 | trustedKeys := make(map[uint16]*dns.DNSKEY) 266 | for _, rr := range msg.Answer { 267 | if rr.Header().Rrtype == dns.TypeDNSKEY { 268 | key := rr.(*dns.DNSKEY) 269 | trustedKeys[key.KeyTag()] = key 270 | } 271 | } 272 | 273 | return trustedKeys, nil 274 | } 275 | 276 | // IsSubDomainStrict checks if child is indeed a child of the parent. If child and parent 277 | // are the same domain false is returned. 278 | func IsSubDomainStrict(parent, child string) bool { 279 | parentLabels := dns.CountLabel(parent) 280 | childLabels := dns.CountLabel(child) 281 | 282 | return dns.CompareDomainName(parent, child) == parentLabels && 283 | childLabels > parentLabels 284 | } 285 | 286 | // verifySignatures verifies signatures in a message 287 | // and removes any invalid rr sets 288 | func verifySignatures(zone string, qname string, msg *dns.Msg, trustedKeys map[uint16]*dns.DNSKEY, t time.Time, minKeySize int) (bool, error) { 289 | type rrsetId struct { 290 | owner string 291 | t uint16 292 | } 293 | 294 | downgrade := false 295 | var lastErr error 296 | 297 | sections := [][]dns.RR{msg.Answer, msg.Ns, msg.Extra} 298 | 299 | // clear sections 300 | // will fill those as we validate 301 | msg.Answer = []dns.RR{} 302 | msg.Ns = []dns.RR{} 303 | msg.Extra = []dns.RR{} 304 | var delegations []dns.RR 305 | 306 | for sectionId, section := range sections { 307 | if len(section) == 0 { 308 | continue 309 | } 310 | 311 | verifiedSets := make(map[rrsetId]struct{}) 312 | 313 | // Look for all signatures some may be invalid 314 | // we only need a single valid signature per RRSet 315 | // this will be used to "discover" covered sets 316 | // if some records don't have a signature they must be 317 | // removed from the section 318 | for _, rr := range section { 319 | // keep delegations since they are unsigned 320 | // they will be verified later 321 | if sectionId == 1 /* authority section */ && 322 | rr.Header().Rrtype == dns.TypeNS { 323 | // must be in bailiwick and within qname 324 | if IsSubDomainStrict(zone, rr.Header().Name) && 325 | dns.IsSubDomain(rr.Header().Name, qname) { 326 | delegations = append(delegations, rr) 327 | } 328 | 329 | continue 330 | } 331 | 332 | if rr.Header().Rrtype == dns.TypeRRSIG { 333 | if sig, ok := rr.(*dns.RRSIG); ok { 334 | sigName := dns.CanonicalName(sig.Header().Name) 335 | 336 | // if another sig verified the set ignore this one 337 | if _, ok := verifiedSets[rrsetId{sigName, sig.TypeCovered}]; ok { 338 | continue 339 | } 340 | 341 | // we don't care about signatures not in bailiwick 342 | if !dns.IsSubDomain(zone, sigName) { 343 | lastErr = ErrSignatureBailiwick 344 | continue 345 | } 346 | 347 | // look for any valid keys for this signature 348 | key, ok := trustedKeys[sig.KeyTag] 349 | // RFC4035 5.3.1 bullet 2 signer name must match the name of the zone 350 | if !ok || !strings.EqualFold(key.Header().Name, sig.SignerName) { 351 | lastErr = ErrMissingDNSKEY 352 | continue 353 | } 354 | 355 | // uses a key that can be downgraded 356 | // it should fallback to insecure 357 | // if there are no other secure 358 | // signatures that can verify the set 359 | if shouldDowngradeKey(key, minKeySize) { 360 | downgrade = true 361 | continue 362 | } 363 | 364 | // extract set covered by signature 365 | rrset := extractRRSet(section, sig.Header().Name, sig.TypeCovered) 366 | if len(rrset) == 0 { 367 | lastErr = ErrMissingSigned 368 | continue 369 | } 370 | 371 | if err := sig.Verify(key, rrset); err != nil { 372 | lastErr = err 373 | continue 374 | } 375 | 376 | if !sig.ValidityPeriod(t) { 377 | lastErr = ErrInvalidSignaturePeriod 378 | continue 379 | } 380 | 381 | // verified 382 | verifiedSets[rrsetId{sigName, sig.TypeCovered}] = struct{}{} 383 | 384 | if sectionId == 0 { 385 | msg.Answer = append(msg.Answer, rrset...) 386 | msg.Answer = append(msg.Answer, sig) 387 | continue 388 | } 389 | 390 | if sectionId == 1 { 391 | msg.Ns = append(msg.Ns, rrset...) 392 | msg.Ns = append(msg.Ns, sig) 393 | continue 394 | } 395 | 396 | msg.Extra = append(msg.Extra, rrset...) 397 | msg.Extra = append(msg.Extra, sig) 398 | } 399 | } 400 | } 401 | } 402 | 403 | if len(msg.Answer) > 0 || len(msg.Ns) > 0 { 404 | // append any unsigned delegations 405 | // to the authority section 406 | if len(delegations) > 0 { 407 | msg.Ns = append(msg.Ns, delegations...) 408 | } 409 | 410 | return true, nil 411 | } 412 | 413 | // we don't have any secure validation paths 414 | // if its okay to downgrade mark zone as insecure 415 | if downgrade { 416 | msg.Answer = sections[0] 417 | msg.Ns = sections[1] 418 | msg.Extra = sections[2] 419 | return false, nil 420 | } 421 | 422 | if lastErr != nil { 423 | return false, fmt.Errorf("error verifying signatures: %v", lastErr) 424 | } 425 | 426 | return false, ErrNoSignatures 427 | } 428 | 429 | func Verify(msg *dns.Msg, zone, qname string, qtype uint16, trustedKeys map[uint16]*dns.DNSKEY, t time.Time, minRSA int) (bool, error) { 430 | if !dns.IsFqdn(zone) || !dns.IsFqdn(qname) { 431 | return false, fmt.Errorf("zone and qname must be fqdn") 432 | } 433 | 434 | secure, err := verifySignatures(zone, qname, msg, trustedKeys, t, minRSA) 435 | if err != nil { 436 | return false, err 437 | } 438 | if !secure { 439 | return false, nil 440 | } 441 | 442 | // signatures are good verify answer 443 | if msg.Rcode == dns.RcodeSuccess { 444 | if len(msg.Answer) == 0 { 445 | return verifyNoData(msg, zone, qname, qtype) 446 | } 447 | 448 | return verifyAnswer(msg, qname, qtype) 449 | } 450 | 451 | if msg.Rcode == dns.RcodeNameError { 452 | return verifyNameError(msg, zone, qname) 453 | } 454 | 455 | return false, fmt.Errorf("unexpected rcode %v", msg.Rcode) 456 | } 457 | 458 | // verifyAnswer pass a verified msg with fqdn canonical qname 459 | func verifyAnswer(msg *dns.Msg, qname string, qtype uint16) (bool, error) { 460 | if len(msg.Answer) == 0 { 461 | return false, errors.New("empty answer") 462 | } 463 | 464 | wildcard := false 465 | nx := false 466 | labels := uint8(dns.CountLabel(qname)) 467 | 468 | // sanitized answer section 469 | var answer []dns.RR 470 | 471 | for _, rr := range msg.Answer { 472 | t := rr.Header().Rrtype 473 | owner := rr.Header().Name 474 | 475 | if t == qtype || t == dns.TypeCNAME { 476 | // only include rrs that match owner name 477 | // TODO: flatten CNAMEs if possible 478 | if strings.EqualFold(qname, owner) { 479 | answer = append(answer, rr) 480 | } 481 | continue 482 | } 483 | 484 | if t == dns.TypeRRSIG && strings.EqualFold(qname, owner) { 485 | sig := rr.(*dns.RRSIG) 486 | if sig.TypeCovered != qtype && 487 | sig.TypeCovered != dns.TypeCNAME { 488 | continue 489 | } 490 | 491 | answer = append(answer, rr) 492 | if sig.Labels < labels { 493 | wildcard = true 494 | } 495 | continue 496 | } 497 | } 498 | 499 | if len(answer) == 0 { 500 | return false, errors.New("empty answer") 501 | } 502 | 503 | msg.Answer = answer 504 | 505 | // if the rrsig is for a wildcard 506 | // there must be an NSEC proving the original name 507 | // doesn't exist 508 | if wildcard { 509 | for _, rr := range msg.Ns { 510 | if rr.Header().Rrtype != dns.TypeNSEC { 511 | continue 512 | } 513 | 514 | nsec := rr.(*dns.NSEC) 515 | if nx = covers(nsec.Header().Name, nsec.NextDomain, qname); nx { 516 | break 517 | } 518 | } 519 | 520 | if !nx { 521 | return false, fmt.Errorf("bad wildcard substitution") 522 | } 523 | } 524 | 525 | return true, nil 526 | } 527 | 528 | func verifyNoData(msg *dns.Msg, zone, qname string, qtype uint16) (bool, error) { 529 | if len(msg.Ns) == 0 { 530 | return false, fmt.Errorf("no nsec records found") 531 | } 532 | 533 | for _, rr := range msg.Ns { 534 | // no authenticated denial of existence 535 | // for NSEC3 for now it should be downgraded 536 | if rr.Header().Rrtype == dns.TypeNSEC3 { 537 | // must be in bailiwick already checked 538 | // by verifySignatures 539 | if dns.IsSubDomain(zone, rr.Header().Name) { 540 | return false, nil 541 | } 542 | } 543 | 544 | if rr.Header().Rrtype == dns.TypeDS { 545 | hasNs := false 546 | 547 | if !IsSubDomainStrict(zone, rr.Header().Name) { 548 | return false, fmt.Errorf("ds record must be a child of zone %s", zone) 549 | } 550 | 551 | // NS records aren't signed 552 | // the owner name must still match 553 | // the DS record. 554 | for _, ns := range msg.Ns { 555 | if ns.Header().Rrtype == dns.TypeNS { 556 | hasNs = true 557 | if !strings.EqualFold(ns.Header().Name, rr.Header().Name) { 558 | return false, fmt.Errorf("bad referral DS owner doesn't match NS") 559 | } 560 | } 561 | } 562 | 563 | // secure delegation with valid 564 | // NS records 565 | if hasNs { 566 | return true, nil 567 | } 568 | 569 | return false, fmt.Errorf("DS record exists without a delegation") 570 | } 571 | 572 | if rr.Header().Rrtype == dns.TypeNSEC { 573 | if nsec, ok := rr.(*dns.NSEC); ok { 574 | // RFC4035 5.4 bullet 1 575 | if !strings.EqualFold(nsec.Header().Name, qname) { 576 | // owner name doesn't match 577 | // RFC4035 5.4 bullet 2 578 | return verifyNameError(msg, zone, qname) 579 | } 580 | 581 | // nsec matches qname 582 | // next domain must be in bailiwick 583 | if !dns.IsSubDomain(zone, nsec.NextDomain) { 584 | continue 585 | } 586 | 587 | hasDelegation := false 588 | hasDS := false 589 | 590 | for _, t := range nsec.TypeBitMap { 591 | if t == qtype { 592 | return false, fmt.Errorf("type exists") 593 | } 594 | if t == dns.TypeCNAME { 595 | return false, fmt.Errorf("cname exists") 596 | } 597 | 598 | if t == dns.TypeDS { 599 | hasDS = true 600 | continue 601 | } 602 | 603 | if t == dns.TypeNS { 604 | hasDelegation = true 605 | } 606 | } 607 | 608 | // verify delegation 609 | for _, nsRR := range msg.Ns { 610 | if nsRR.Header().Rrtype == dns.TypeNS { 611 | if hasDS { 612 | return false, fmt.Errorf("bad insecure delegation proof " + 613 | "DS exists in NSEC bitmap") 614 | } 615 | if !hasDelegation { 616 | return false, fmt.Errorf("NS isn't set in NSEC bitmap") 617 | } 618 | if !strings.EqualFold(nsRR.Header().Name, nsec.Header().Name) { 619 | return false, fmt.Errorf("invalid NS owner name") 620 | } 621 | if strings.EqualFold(nsRR.Header().Name, zone) { 622 | return false, fmt.Errorf("bad referral") 623 | } 624 | } 625 | } 626 | 627 | return true, nil 628 | } 629 | } 630 | } 631 | 632 | return false, fmt.Errorf("no valid nsec records found") 633 | } 634 | 635 | func verifyNameError(msg *dns.Msg, zone, qname string) (bool, error) { 636 | nameProof := false 637 | wildcardProof := false 638 | qnameParts := dns.SplitDomainName(qname) 639 | for _, rr := range msg.Ns { 640 | if nameProof && wildcardProof { 641 | break 642 | } 643 | 644 | if rr.Header().Rrtype == dns.TypeNSEC { 645 | nsec, ok := rr.(*dns.NSEC) 646 | if !ok { 647 | continue 648 | } 649 | 650 | if !nameProof && covers(nsec.Header().Name, nsec.NextDomain, qname) { 651 | nameProof = true 652 | } 653 | 654 | if c, err := canonicalNameCompare(nsec.Header().Name, nsec.NextDomain); err == nil && c < 0 { 655 | if IsSubDomainStrict(qname, nsec.NextDomain) { 656 | wildcardProof = true 657 | continue 658 | } 659 | } 660 | 661 | if !wildcardProof { 662 | // find closest wildcard proof that covers qname 663 | i := 1 664 | for { 665 | if len(qnameParts) < i { 666 | break 667 | } 668 | 669 | domain := dns.Fqdn("*." + strings.Join(qnameParts[i:], ".")) 670 | if !dns.IsSubDomain(zone, domain) { 671 | break 672 | } 673 | if covers(nsec.Header().Name, nsec.NextDomain, domain) { 674 | wildcardProof = true 675 | break 676 | } 677 | i++ 678 | } 679 | } 680 | } 681 | } 682 | 683 | if !nameProof { 684 | return false, fmt.Errorf("missing name proof") 685 | } 686 | 687 | if !wildcardProof { 688 | return false, fmt.Errorf("missing wildcard proof") 689 | } 690 | 691 | return true, nil 692 | } 693 | 694 | // RFC4034 6.1. Canonical DNS Name Order 695 | // https://tools.ietf.org/html/rfc4034#section-6.1 696 | // Returns -1 if name1 comes before name2, 1 if name1 comes after name2, and 0 if they are equal. 697 | func canonicalNameCompare(name1 string, name2 string) (int, error) { 698 | // TODO: optimize comparison 699 | name1 = dns.Fqdn(name1) 700 | name2 = dns.Fqdn(name2) 701 | 702 | if _, ok := dns.IsDomainName(name1); !ok { 703 | return 0, errors.New("invalid domain name") 704 | } 705 | if _, ok := dns.IsDomainName(name2); !ok { 706 | return 0, errors.New("invalid domain name") 707 | } 708 | 709 | labels1 := dns.SplitDomainName(name1) 710 | labels2 := dns.SplitDomainName(name2) 711 | 712 | var buf1, buf2 [64]byte 713 | // start comparison from the right 714 | currentLabel1, currentLabel2, min := len(labels1)-1, len(labels2)-1, 0 715 | 716 | if min = currentLabel1; min > currentLabel2 { 717 | min = currentLabel2 718 | } 719 | 720 | for i := min; i > -1; i-- { 721 | off1, err := dns.PackDomainName(labels1[currentLabel1]+".", buf1[:], 0, nil, false) 722 | if err != nil { 723 | return 0, err 724 | } 725 | 726 | off2, err := dns.PackDomainName(labels2[currentLabel2]+".", buf2[:], 0, nil, false) 727 | if err != nil { 728 | return 0, err 729 | } 730 | 731 | currentLabel1-- 732 | currentLabel2-- 733 | 734 | // if the two labels at the same index aren't equal return result 735 | if res := bytes.Compare(bytes.ToLower(buf1[1:off1-1]), 736 | bytes.ToLower(buf2[1:off2-1])); res != 0 { 737 | return res, nil 738 | } 739 | } 740 | 741 | // all labels are equal name with least labels is the smallest 742 | if len(labels1) == len(labels2) { 743 | return 0, nil 744 | } 745 | 746 | if len(labels1)-1 == min { 747 | return -1, nil 748 | } 749 | 750 | return 1, nil 751 | } 752 | 753 | func covers(owner, next, qname string) (result bool) { 754 | var errs int 755 | 756 | // qname is equal to or before owner can't be covered 757 | if compareWithErrors(qname, owner, &errs) <= 0 { 758 | return false 759 | } 760 | 761 | lastNSEC := compareWithErrors(owner, next, &errs) >= 0 762 | inRange := lastNSEC || compareWithErrors(qname, next, &errs) < 0 763 | if !inRange { 764 | return false 765 | } 766 | 767 | if errs > 0 { 768 | return false 769 | } 770 | 771 | return true 772 | } 773 | 774 | func compareWithErrors(a, b string, errs *int) int { 775 | res, err := canonicalNameCompare(a, b) 776 | if err != nil { 777 | *errs++ 778 | } 779 | 780 | return res 781 | } 782 | 783 | func extractRRSet(in []dns.RR, name string, types ...uint16) []dns.RR { 784 | var out []dns.RR 785 | tMap := make(map[uint16]struct{}, len(types)) 786 | for _, t := range types { 787 | tMap[t] = struct{}{} 788 | } 789 | for _, r := range in { 790 | if _, ok := tMap[r.Header().Rrtype]; ok { 791 | if name != "" && !strings.EqualFold(name, r.Header().Name) { 792 | continue 793 | } 794 | out = append(out, r) 795 | } 796 | } 797 | return out 798 | } 799 | --------------------------------------------------------------------------------