From a93ed82d9aeae5cffdcfd88d26eab6367b187d42 Mon Sep 17 00:00:00 2001 From: Riccardo Bessone Date: Wed, 23 Sep 2026 12:52:14 +0200 Subject: [PATCH 1/3] Implement DNSSEC status in whois style Add DNSSEC status handling to response output in whois style --- response.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/response.go b/response.go index 8756f44..f507862 100644 --- a/response.go +++ b/response.go @@ -111,6 +111,15 @@ func (r *Response) ToWhoisStyleResponse() *WhoisStyleResponse { w.add("Domain Status", s) } + // "DNSSEC" + if d.SecureDNS != nil && d.SecureDNS.DelegationSigned != nil { + if *d.SecureDNS.DelegationSigned { + w.add("DNSSEC", "signedDelegation") + } else { + w.add("DNSSEC", "unsigned") + } + } + addEntityFields(w, "Registrant", findFirstEntity("registrant", d.Entities)) addEntityFields(w, "Admin", findFirstEntity("administrative", d.Entities)) addEntityFields(w, "Tech", findFirstEntity("technical", d.Entities)) From bc63f2026085ba1246b2f997e92df3baa29552ee Mon Sep 17 00:00:00 2001 From: Robert Thomas Date: Wed, 23 Sep 2026 21:13:00 +0100 Subject: [PATCH 2/3] fix: place DNSSEC after Name Server and infer from DS/key data Match ICANN WHOIS field order, fall back to dsData/keyData presence when delegationSigned is omitted, and add tests. --- response.go | 24 +++++++++++++++--------- response_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 response_test.go diff --git a/response.go b/response.go index f507862..d2bc3c4 100644 --- a/response.go +++ b/response.go @@ -111,15 +111,6 @@ func (r *Response) ToWhoisStyleResponse() *WhoisStyleResponse { w.add("Domain Status", s) } - // "DNSSEC" - if d.SecureDNS != nil && d.SecureDNS.DelegationSigned != nil { - if *d.SecureDNS.DelegationSigned { - w.add("DNSSEC", "signedDelegation") - } else { - w.add("DNSSEC", "unsigned") - } - } - addEntityFields(w, "Registrant", findFirstEntity("registrant", d.Entities)) addEntityFields(w, "Admin", findFirstEntity("administrative", d.Entities)) addEntityFields(w, "Tech", findFirstEntity("technical", d.Entities)) @@ -130,6 +121,21 @@ func (r *Response) ToWhoisStyleResponse() *WhoisStyleResponse { w.add("Name Server", n.LDHName) } + // "DNSSEC" + if s := d.SecureDNS; s != nil { + switch { + case s.DelegationSigned != nil: + if *s.DelegationSigned { + w.add("DNSSEC", "signedDelegation") + } else { + w.add("DNSSEC", "unsigned") + } + case len(s.DS) > 0 || len(s.Keys) > 0: + // Some servers omit delegationSigned but still publish DS or key data. + w.add("DNSSEC", "signedDelegation") + } + } + return w } diff --git a/response_test.go b/response_test.go new file mode 100644 index 0000000..7bff0b2 --- /dev/null +++ b/response_test.go @@ -0,0 +1,49 @@ +// OpenRDAP +// Copyright 2017 Tom Harwood +// MIT License, see the LICENSE file. + +package rdap + +import "testing" + +func TestWhoisStyleDNSSEC(t *testing.T) { + yes, no := true, false + + tests := []struct { + name string + secureDNS *SecureDNS + want []string + }{ + {"no secureDNS", nil, nil}, + {"signed", &SecureDNS{DelegationSigned: &yes}, []string{"signedDelegation"}}, + {"unsigned", &SecureDNS{DelegationSigned: &no}, []string{"unsigned"}}, + {"unsigned wins over DS", &SecureDNS{DelegationSigned: &no, DS: []DSData{{}}}, []string{"unsigned"}}, + {"DS without delegationSigned", &SecureDNS{DS: []DSData{{}}}, []string{"signedDelegation"}}, + {"keyData without delegationSigned", &SecureDNS{Keys: []KeyData{{}}}, []string{"signedDelegation"}}, + {"empty secureDNS", &SecureDNS{}, nil}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Response{Object: &Domain{ + LDHName: "example.com", + Nameservers: []Nameserver{{LDHName: "ns1.example.com"}}, + SecureDNS: tt.secureDNS, + }} + w := r.ToWhoisStyleResponse() + + got := w.Data["DNSSEC"] + if len(got) != len(tt.want) || (len(got) == 1 && got[0] != tt.want[0]) { + t.Fatalf("DNSSEC = %v, want %v", got, tt.want) + } + + // ICANN WHOIS ordering places DNSSEC directly after the name servers. + if tt.want != nil { + order := w.KeyDisplayOrder + if order[len(order)-1] != "DNSSEC" || order[len(order)-2] != "Name Server" { + t.Fatalf("KeyDisplayOrder = %v, want DNSSEC after Name Server", order) + } + } + }) + } +} From fb41f508d1dce7d31051b2d34cfd884e310d9ca0 Mon Sep 17 00:00:00 2001 From: Robert Thomas Date: Wed, 23 Sep 2026 21:32:37 +0100 Subject: [PATCH 3/3] fix: don't infer signed delegation from keyData keyData is child DNSKEY data; only DS records prove the parent delegation is signed (RFC 9083). --- response.go | 6 ++++-- response_test.go | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/response.go b/response.go index d2bc3c4..47c64be 100644 --- a/response.go +++ b/response.go @@ -130,8 +130,10 @@ func (r *Response) ToWhoisStyleResponse() *WhoisStyleResponse { } else { w.add("DNSSEC", "unsigned") } - case len(s.DS) > 0 || len(s.Keys) > 0: - // Some servers omit delegationSigned but still publish DS or key data. + case len(s.DS) > 0: + // Some servers omit delegationSigned but still publish DS records. + // keyData is deliberately ignored: it is child DNSKEY data and does + // not prove the parent has published DS records. w.add("DNSSEC", "signedDelegation") } } diff --git a/response_test.go b/response_test.go index 7bff0b2..b14c6a9 100644 --- a/response_test.go +++ b/response_test.go @@ -19,7 +19,7 @@ func TestWhoisStyleDNSSEC(t *testing.T) { {"unsigned", &SecureDNS{DelegationSigned: &no}, []string{"unsigned"}}, {"unsigned wins over DS", &SecureDNS{DelegationSigned: &no, DS: []DSData{{}}}, []string{"unsigned"}}, {"DS without delegationSigned", &SecureDNS{DS: []DSData{{}}}, []string{"signedDelegation"}}, - {"keyData without delegationSigned", &SecureDNS{Keys: []KeyData{{}}}, []string{"signedDelegation"}}, + {"keyData without delegationSigned", &SecureDNS{Keys: []KeyData{{}}}, nil}, {"empty secureDNS", &SecureDNS{}, nil}, }