From 2265bc5375af9a226b5a44fc538e9de13f599c81 Mon Sep 17 00:00:00 2001 From: Henry Date: Sun, 9 Jun 2024 18:25:26 +0200 Subject: [PATCH] start JS/WASM wrapper functions --- golang/cmd/sig0namectl/request_key.go | 13 +- golang/sig0/answers.go | 27 ++-- golang/sig0/query_test.go | 4 +- golang/sig0/request_key.go | 63 ++++++--- golang/sig0/request_key_test.go | 4 +- golang/wasm/q_js.go | 97 -------------- golang/wasm/wrapper_js.go | 185 ++++++++++++++++++++++++++ 7 files changed, 261 insertions(+), 132 deletions(-) delete mode 100644 golang/wasm/q_js.go create mode 100644 golang/wasm/wrapper_js.go diff --git a/golang/cmd/sig0namectl/request_key.go b/golang/cmd/sig0namectl/request_key.go index 26c3c3b..1ae2ca1 100644 --- a/golang/cmd/sig0namectl/request_key.go +++ b/golang/cmd/sig0namectl/request_key.go @@ -12,23 +12,22 @@ import ( var requestKeyCmd = &cli.Command{ Name: "requestKey", - Usage: "requestKey ", + Usage: "requestKey ", Aliases: []string{"rk"}, Action: requestKeyAction, } func requestKeyAction(cCtx *cli.Context) error { - newSubZone := cCtx.Args().Get(0) - zone := cCtx.Args().Get(1) - if newSubZone == "" || zone == "" { - return cli.Exit("subZone and zone are required", 1) + newName := cCtx.Args().Get(0) + if newName == "" { + return cli.Exit("newName required", 1) } - reqMsg, dohServer, err := sig0.CreateRequestKeyMsg(newSubZone, zone) + reqMsg, dohServer, err := sig0.CreateRequestKeyMsg(newName) if err != nil { return fmt.Errorf("Failed to create request key message: %w", err) } - log.Println("Requesting key for", newSubZone, "under", zone, "from", dohServer) + log.Println("Requesting key for", newName, "from", dohServer) spew.Dump(reqMsg) answer, err := sig0.SendDOHQuery(dohServer, reqMsg) diff --git a/golang/sig0/answers.go b/golang/sig0/answers.go index 38a4f2f..727ebcb 100644 --- a/golang/sig0/answers.go +++ b/golang/sig0/answers.go @@ -2,6 +2,7 @@ package sig0 import ( "encoding/base64" + "errors" "fmt" "github.com/miekg/dns" @@ -20,26 +21,36 @@ func ParseBase64Answer(answer string) (*dns.Msg, error) { return resp, nil } -func ExpectSOA(answer *dns.Msg) (string, error) { +func ExpectSOA(answer *dns.Msg) (*dns.SOA, error) { if len(answer.Answer) < 1 { - return "", fmt.Errorf("expected at least one authority section.") + return nil, fmt.Errorf("expected at least one answer section.") } firstNS := answer.Answer[0] soa, ok := firstNS.(*dns.SOA) if !ok { - return "", fmt.Errorf("expected SOA but got type of RR: %T: %+v", firstNS, firstNS) + return nil, fmt.Errorf("expected SOA but got type of RR: %T: %+v", firstNS, firstNS) } - return soa.Ns, nil + return soa, nil } -func ExpectAdditonalSOA(answer *dns.Msg) (string, error) { +func ExpectAdditonalSOA(answer *dns.Msg) (*dns.SOA, error) { if len(answer.Ns) < 1 { - return "", fmt.Errorf("expected at least one authority section.") + return nil, fmt.Errorf("expected at least one authority section.") } firstNS := answer.Ns[0] soa, ok := firstNS.(*dns.SOA) if !ok { - return "", fmt.Errorf("expected SOA but got type of RR: %T: %+v", firstNS, firstNS) + return nil, fmt.Errorf("expected SOA but got type of RR: %T: %+v", firstNS, firstNS) } - return soa.Ns, nil + return soa, nil +} + +func AnySOA(answer *dns.Msg) (*dns.SOA, error) { + if soa, err := ExpectSOA(answer); err == nil { + return soa, nil + } + if soa, err := ExpectAdditonalSOA(answer); err == nil { + return soa, nil + } + return nil, errors.New("no SOA in either answer or additional") } diff --git a/golang/sig0/query_test.go b/golang/sig0/query_test.go index 6deb083..eeddee2 100644 --- a/golang/sig0/query_test.go +++ b/golang/sig0/query_test.go @@ -35,12 +35,12 @@ func TestQuerySOA(t *testing.T) { soa, err := ExpectSOA(answer) r.NoError(err, testdata) - a.Equal(testdata.soa, soa) + a.Equal(testdata.soa, soa.Ns) verifyication, err := QueryWithType(testdata.zone, dns.TypeSOA) r.NoError(err, testdata) - verifyAnswer, err := SendUDPQuery(soa, verifyication) + verifyAnswer, err := SendUDPQuery(soa.Ns, verifyication) r.NoError(err, testdata) r.True(verifyAnswer.Authoritative, verifyAnswer) } diff --git a/golang/sig0/request_key.go b/golang/sig0/request_key.go index dd45afb..264e2b4 100644 --- a/golang/sig0/request_key.go +++ b/golang/sig0/request_key.go @@ -3,6 +3,7 @@ package sig0 import ( "errors" "fmt" + "log" "strings" "github.com/miekg/dns" @@ -14,10 +15,37 @@ var ( DefaultDOHResolver = "dns.quad9.net" ) -func CreateRequestKeyMsg(subZone, zone string) (*dns.Msg, string, error) { +func CreateRequestKeyMsg(newName string) (*dns.Msg, string, error) { + querySOAForNewZone, err := QuerySOA(newName) + if err != nil { + return nil, "", fmt.Errorf("Error: ZONE %s SOA record does not resolve: %w", newName, err) + } + + newZoneSOAAnswer, err := SendDOHQuery(DefaultDOHResolver, querySOAForNewZone) + if err != nil { + return nil, "", fmt.Errorf("Error: DOH query failed for %s: %w", DefaultDOHResolver, err) + } + + soaForZone, err := AnySOA(newZoneSOAAnswer) + if err != nil { + return nil, "", fmt.Errorf("Error: SOA record not found in response for %s: %w", newName, err) + } + + zoneOfName := soaForZone.Hdr.Name + log.Printf("[requestKey] Found zone for new name: %s", zoneOfName) + + newNameFQDN := newName + if !strings.HasSuffix(newNameFQDN, ".") { + newNameFQDN += "." + } + + if !strings.HasSuffix(newNameFQDN, zoneOfName) { + return nil, "", fmt.Errorf("Error: expected new zone to be under it's SOA. Instead got SOA %q for %q", zoneOfName, newNameFQDN) + } + subDomain := strings.TrimSuffix(newNameFQDN, zoneOfName) // Determine the zone master using the provided sub zone and base zone - signalZone := fmt.Sprintf("%s.%s", SignalSubzonePrefix, zone) + signalZone := fmt.Sprintf("%s.%s", SignalSubzonePrefix, zoneOfName) querySOAForSignal, err := QuerySOA(signalZone) if err != nil { return nil, "", fmt.Errorf("Error: ZONE %s SOA record does not resolve: %w", signalZone, err) @@ -28,38 +56,41 @@ func CreateRequestKeyMsg(subZone, zone string) (*dns.Msg, string, error) { return nil, "", fmt.Errorf("Error: DOH query failed for %s: %w", DefaultDOHResolver, err) } - zoneSoa, err := ExpectSOA(soaAnswer) + signalZoneSoa, err := ExpectSOA(soaAnswer) if err != nil { return nil, "", fmt.Errorf("Error: SOA record not found in response for %s: %w", signalZone, err) } - if zoneSoa != "ns1.free2air.org." { - return nil, "", fmt.Errorf("Unexpected SOA: %s - TODO: Query SVCB to get the zone master's DOH endpoint", zoneSoa) + if !strings.HasSuffix(signalZoneSoa.Hdr.Name, soaForZone.Hdr.Name) { + return nil, "", fmt.Errorf("Expected signal zone to be under requested zonet got %q and %q", signalZoneSoa.Hdr.Name, soaForZone.Hdr.Name) + } + + if signalZoneSoa.Ns != "ns1.free2air.org." { + return nil, "", fmt.Errorf("Unexpected SOA: %s - TODO: Query SVCB to get the zone master's DOH endpoint", zoneOfName) } var dohUpdateHost = "doh.zenr.io" // Check if zone already exists - newSubZone := fmt.Sprintf("%s.%s", subZone, zone) - err = checkZoneDoesntExist(dohUpdateHost, newSubZone) + err = checkZoneDoesntExist(dohUpdateHost, newName) if err != nil { - return nil, "", err + return nil, "", fmt.Errorf("exists check for new name %q failed: %w", newName, err) } - zoneRequest := fmt.Sprintf("%s.%s.%s", subZone, SignalSubzonePrefix, zone) + zoneRequest := fmt.Sprintf("%s%s.%s", subDomain, SignalSubzonePrefix, zoneOfName) err = checkZoneDoesntExist(dohUpdateHost, zoneRequest) if err != nil { - return nil, "", err + return nil, "", fmt.Errorf("exists check for zoneRequest %q failed: %w", zoneRequest, err) } // craft RRs and create signed update - subZoneSigner, err := LoadOrGenerateKey(newSubZone) + subZoneSigner, err := LoadOrGenerateKey(newName) if err != nil { return nil, "", err } - err = subZoneSigner.StartUpdate(zone) + err = subZoneSigner.StartUpdate(zoneOfName) if err != nil { - return nil, "", err + return nil, "", fmt.Errorf("unable to start update for zone: %q: %w", zoneOfName, err) } // Here we split the key details @@ -75,18 +106,18 @@ func CreateRequestKeyMsg(subZone, zone string) (*dns.Msg, string, error) { nsupdateItemSig0Key := fmt.Sprintf("%s %d %s", zoneRequest, DefaultTTL, keyData) err = subZoneSigner.UpdateParsedRR(nsupdateItemSig0Key) if err != nil { - return nil, "", err + return nil, "", fmt.Errorf("failed to add KEY RR: %w", err) } nsupdateItemPtr := fmt.Sprintf("%s %d IN PTR %s", signalZone, DefaultTTL, zoneRequest) err = subZoneSigner.UpdateParsedRR(nsupdateItemPtr) if err != nil { - return nil, "", err + return nil, "", fmt.Errorf("failed to add PTR RR: %w", err) } updateMsg, err := subZoneSigner.UnsignedUpdate(signalZone) if err != nil { - return nil, "", err + return nil, "", fmt.Errorf("unable to create update message") } return updateMsg, dohUpdateHost, nil diff --git a/golang/sig0/request_key_test.go b/golang/sig0/request_key_test.go index c44befe..b4b29d9 100644 --- a/golang/sig0/request_key_test.go +++ b/golang/sig0/request_key_test.go @@ -13,9 +13,9 @@ func TestRequestKey(t *testing.T) { buf := make([]byte, 5) rand.Read(buf) - testSubZone := fmt.Sprintf("sig0namectl-test-%x", buf) + testName := fmt.Sprintf("sig0namectl-test-%x.zenr.io", buf) - zoneRequestMsg, dohServer, err := CreateRequestKeyMsg(testSubZone, "zenr.io") + zoneRequestMsg, dohServer, err := CreateRequestKeyMsg(testName) r.NoError(err) r.Equal("doh.zenr.io", dohServer) t.Log(zoneRequestMsg) diff --git a/golang/wasm/q_js.go b/golang/wasm/q_js.go deleted file mode 100644 index a0955a0..0000000 --- a/golang/wasm/q_js.go +++ /dev/null @@ -1,97 +0,0 @@ -package main - -import ( - "encoding/base64" - "fmt" - "log" - "syscall/js" - - "github.com/davecgh/go-spew/spew" - - "github.com/NetworkCommons/sig0namectl/sig0" -) - -func main() { - var ( - query, queryFromDOM js.Func - parse, update js.Func - ) - - queryFromDOM = js.FuncOf(func(this js.Value, args []js.Value) any { - domainName := js.Global().Get("document").Call("getElementById", "domain-name").Get("value") - - fmt.Println("Domain:", domainName) - q, err := sig0.QueryAny(domainName.String()) - check(err) - js.Global().Get("document").Call("getElementById", "query-data").Set("value", q) - return q - }) - js.Global().Get("document").Call("getElementById", "prepare").Call("addEventListener", "click", queryFromDOM) - - query = js.FuncOf(func(this js.Value, args []js.Value) any { - if len(args) != 1 { - panic("expected 1 argument") - } - domainName := args[0].String() - fmt.Println("Domain:", domainName) - q, err := sig0.QueryAny(domainName) - check(err) - return q - }) - - parse = js.FuncOf(func(this js.Value, args []js.Value) any { - answer := js.Global().Get("document").Call("getElementById", "dns-answer").Get("value") - msg, err := sig0.ParseBase64Answer(answer.String()) - check(err) - js.Global().Get("document").Call("getElementById", "pretty").Set("innerHTML", spew.Sdump(msg)) - return "" - }) - js.Global().Get("document").Call("getElementById", "parse-answer").Call("addEventListener", "click", parse) - - update = js.FuncOf(func(this js.Value, args []js.Value) any { - if len(args) != 1 || args[0].Type() != js.TypeString { - check(fmt.Errorf("expected 1 string argument")) - return "" - } - - // TODO: remove hardcoding - zone := "cryptix.zenr.io" - signer, err := sig0.LoadOrGenerateKey(zone) - check(err) - - log.Println("signer loaded", signer.Key.Hdr.Name, signer.Key.KeyTag()) - - err = signer.StartUpdate(zone) - check(err) - - err = signer.UpdateA("cryptix", "zenr.io", args[0].String()) - check(err) - - m, err := signer.SignUpdate() - check(err) - - enc, err := m.Pack() - check(err) - - return base64.StdEncoding.EncodeToString(enc) - }) - - // setup functions for access from js side - goFuncs := js.Global().Get("window").Get("goFuncs") - goFuncs.Set("query", query) - goFuncs.Set("parse", parse) - goFuncs.Set("update", update) - - // cant let main return - forever := make(chan bool) - select { - case <-forever: - } -} - -func check(err error) { - if err != nil { - js.Global().Call("alert", err.Error()) - panic(err) - } -} diff --git a/golang/wasm/wrapper_js.go b/golang/wasm/wrapper_js.go new file mode 100644 index 0000000..525b1fd --- /dev/null +++ b/golang/wasm/wrapper_js.go @@ -0,0 +1,185 @@ +package main + +import ( + "fmt" + "syscall/js" + + "github.com/miekg/dns" + + "github.com/NetworkCommons/sig0namectl/sig0" +) + +// Go <-> JS bridging setup +// ======================== + +func main() { + // setup functions for access from js side + goFuncs := js.Global().Get("window").Get("goFuncs") + goFuncs.Set("setDefaultDOHResolver", js.FuncOf(setDefaultDOHResolver)) + + goFuncs.Set("listKeys", js.FuncOf(listKeys)) + goFuncs.Set("requestKey", js.FuncOf(requestKey)) + goFuncs.Set("newUpdater", js.FuncOf(newUpdater)) + + goFuncs.Set("queryAny", js.FuncOf(queryAny)) + + // cant let main return + forever := make(chan bool) + select { + case <-forever: + } +} + +// the DOH resolver to lookup SOAs and check zones for existence +// returns null or an error string +func setDefaultDOHResolver(_ js.Value, args []js.Value) any { + if len(args) != 1 { + return "expected 1 argument" + } + resolver := args[0].String() + sig0.DefaultDOHResolver = resolver + return js.Null() +} + +// Key Managment +// ============= + +// arguments: 0 +// Returns a list of strings +func listKeys(_ js.Value, _ []js.Value) any { + keys, err := sig0.ListKeys(".") + if err != nil { + panic(err) + } + var values = make([]any, len(keys)) + for i, k := range keys { + values[i] = k + } + return values +} + +// create a keypair and request a key +// arguments: the name to request +// returns nill or an error string +func requestKey(_ js.Value, args []js.Value) any { + if len(args) != 1 { + return "expected 1 argument" + } + domainName := args[0].String() + + msg, soaDOHServer, err := sig0.CreateRequestKeyMsg(domainName) + if err != nil { + return err.Error() + } + + answer, err := sig0.SendDOHQuery(soaDOHServer, msg) + if err != nil { + return err.Error() + } + + if answer.Rcode != dns.RcodeSuccess { + return fmt.Sprintf("did not get success answer\n:%#v", answer) + } + + return js.Null() +} + +// creates a new updater for the passed zone. +// can create signed or unsigned update messages. +// can only create one update. +// +// arg 1: The Zone to update. +// arg 2: The DOH server to send the update to. +// +// returns an object with three functions {addRR, signedUpdate, unsignedUpdate} +func newUpdater(_ js.Value, args []js.Value) any { + if len(args) != 2 { + panic("expected 2 arguments: zone, dohHostname") + } + zone := args[0].String() + dohServer := args[1].String() + + signer, err := sig0.LoadOrGenerateKey(zone) + if err != nil { + panic(fmt.Errorf("failed to load key: %w", err)) + } + + err = signer.StartUpdate(zone) + if err != nil { + panic(fmt.Errorf("failed to start update: %w", err)) + } + + return map[string]any{ + // addRR + // 1 argument: the RR string + // returns null or an error string + "addRR": js.FuncOf(func(this js.Value, args []js.Value) any { + rr := args[0].String() + err := signer.UpdateParsedRR(rr) + if err != nil { + return err.Error() + } + return js.Null() + }), + + // send signed update + // no arguments + // returns null or an error string + "signedUpdate": js.FuncOf(func(this js.Value, _ []js.Value) any { + msg, err := signer.SignUpdate() + if err != nil { + return err.Error() + } + answer, err := sig0.SendDOHQuery(dohServer, msg) + if err != nil { + return err.Error() + } + if answer.Rcode != dns.RcodeSuccess { + return fmt.Sprintf("did not get success answer\n:%#v", answer) + } + return js.Null() + }), + + // send unsigned update + // no arguments + // returns null or an error string + "unsignedUpdate": js.FuncOf(func(this js.Value, _ []js.Value) any { + msg, err := signer.UnsignedUpdate(zone) + if err != nil { + return err.Error() + } + answer, err := sig0.SendDOHQuery(dohServer, msg) + if err != nil { + return err.Error() + } + if answer.Rcode != dns.RcodeSuccess { + return fmt.Sprintf("did not get success answer\n:%#v", answer) + } + return js.Null() + }), + } +} + +// queries +// ======= + +func queryAny(this js.Value, args []js.Value) any { + if len(args) != 1 { + panic("expected 1 argument") + } + domainName := args[0].String() + fmt.Println("Domain:", domainName) + q, err := sig0.QueryAny(domainName) + check(err) + return q +} + +// Utilities +// ========= + +func check(err error) { + if err != nil { + js.Global().Call("alert", err.Error()) + panic(err) + } +}