|
| 1 | +package dnsutils |
| 2 | + |
| 3 | +import ( |
| 4 | + "crypto" |
| 5 | + "fmt" |
| 6 | + "io" |
| 7 | + "sort" |
| 8 | + "time" |
| 9 | + |
| 10 | + "github.com/miekg/dns" |
| 11 | +) |
| 12 | + |
| 13 | +type DenialOfExistenceMethod string |
| 14 | + |
| 15 | +const ( |
| 16 | + DenialOfExistenceMethodNSEC = "NSEC" |
| 17 | + DenialOfExistenceMethodNSEC3 = "NSEC3" |
| 18 | +) |
| 19 | + |
| 20 | +var ( |
| 21 | + DefaultBeforeSign time.Duration = time.Hour |
| 22 | + DefaultExpiry time.Duration = time.Hour * 24 * 14 |
| 23 | +) |
| 24 | + |
| 25 | +type SignOption struct { |
| 26 | + DoEMethod DenialOfExistenceMethod |
| 27 | + BeforeSign *time.Duration |
| 28 | + Expiry *time.Duration |
| 29 | + Inception *uint32 |
| 30 | + Expiration *uint32 |
| 31 | + // TODO: AddCDS bool |
| 32 | + // TODO: AddCDNSKEY bool |
| 33 | +} |
| 34 | + |
| 35 | +func (o *SignOption) GetBeforSign() time.Duration { |
| 36 | + if o.BeforeSign == nil { |
| 37 | + return DefaultBeforeSign |
| 38 | + } |
| 39 | + return *o.BeforeSign |
| 40 | +} |
| 41 | + |
| 42 | +func (o *SignOption) GetExpiry() time.Duration { |
| 43 | + if o.Expiry == nil { |
| 44 | + return DefaultExpiry |
| 45 | + } |
| 46 | + return *o.Expiry |
| 47 | +} |
| 48 | + |
| 49 | +func (o *SignOption) GetInception() uint32 { |
| 50 | + if o.Inception == nil { |
| 51 | + return uint32(time.Now().UTC().Add(-o.GetBeforSign()).Unix()) |
| 52 | + } |
| 53 | + return *o.Inception |
| 54 | +} |
| 55 | + |
| 56 | +func (o *SignOption) GetExpiration() uint32 { |
| 57 | + if o.Expiration == nil { |
| 58 | + return uint32(time.Now().UTC().Add(o.GetExpiry()).Unix()) |
| 59 | + } |
| 60 | + return *o.Expiration |
| 61 | +} |
| 62 | + |
| 63 | +type DNSKEY struct { |
| 64 | + rr *dns.DNSKEY |
| 65 | + signer crypto.Signer |
| 66 | +} |
| 67 | + |
| 68 | +func ReadDNSKEY(priv, pub io.Reader) (*DNSKEY, error) { |
| 69 | + var dnskey *dns.DNSKEY |
| 70 | + zp := dns.NewZoneParser(pub, "", "") |
| 71 | + for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { |
| 72 | + if rr.Header().Rrtype == dns.TypeDNSKEY { |
| 73 | + dnskey, _ = rr.(*dns.DNSKEY) |
| 74 | + } |
| 75 | + } |
| 76 | + if dnskey == nil { |
| 77 | + return nil, fmt.Errorf("DNSKEY not found") |
| 78 | + } |
| 79 | + privateKey, err := dnskey.ReadPrivateKey(priv, "") |
| 80 | + if err != nil { |
| 81 | + return nil, fmt.Errorf("DNSKEY not found") |
| 82 | + } |
| 83 | + signer, _ := privateKey.(crypto.Signer) |
| 84 | + return &DNSKEY{ |
| 85 | + rr: dnskey, |
| 86 | + signer: signer, |
| 87 | + }, nil |
| 88 | +} |
| 89 | + |
| 90 | +func (d *DNSKEY) GetSigner() crypto.Signer { |
| 91 | + return d.signer |
| 92 | +} |
| 93 | + |
| 94 | +func (d *DNSKEY) GetRR() *dns.DNSKEY { |
| 95 | + return d.rr |
| 96 | +} |
| 97 | + |
| 98 | +func (d *DNSKEY) IsKSK() bool { |
| 99 | + return d.rr.Flags == 257 |
| 100 | +} |
| 101 | + |
| 102 | +func (d *DNSKEY) IsZSK() bool { |
| 103 | + return d.rr.Flags == 256 |
| 104 | +} |
| 105 | + |
| 106 | +func AddDNSKEY(z ZoneInterface, dnskeys []*DNSKEY, ttl uint32, generator Generator) error { |
| 107 | + if len(dnskeys) == 0 { |
| 108 | + return fmt.Errorf("empty DNSKEYs") |
| 109 | + } |
| 110 | + if ttl == 0 { |
| 111 | + ttl = 3600 |
| 112 | + } |
| 113 | + rrset, err := GetRRSetOrCreate(z.GetRootNode(), dns.TypeDNSKEY, ttl, generator) |
| 114 | + if err != nil { |
| 115 | + return fmt.Errorf("failed to create DNSKEY rrset: %w", err) |
| 116 | + } |
| 117 | + for _, dnskey := range dnskeys { |
| 118 | + rr := dnskey.GetRR() |
| 119 | + rr.Hdr.Ttl = rrset.GetTTL() |
| 120 | + if err := rrset.AddRR(rr); err != nil { |
| 121 | + return fmt.Errorf("failed to add DNSKEY RR: %w", err) |
| 122 | + } |
| 123 | + } |
| 124 | + if err := z.GetRootNode().SetRRSet(rrset); err != nil { |
| 125 | + return fmt.Errorf("failed to set DNSKEY rrset: %w", err) |
| 126 | + } |
| 127 | + return nil |
| 128 | +} |
| 129 | + |
| 130 | +func SignZone(z ZoneInterface, opt SignOption, dnskeys []*DNSKEY, generator Generator) error { |
| 131 | + if generator == nil { |
| 132 | + generator = &DefaultGenerator{} |
| 133 | + } |
| 134 | + // Sign |
| 135 | + return z.GetRootNode().IterateNameNodeWithValue(func(nni NameNodeInterface, a any) (any, error) { |
| 136 | + auth := a.(bool) |
| 137 | + if z.GetName() != nni.GetName() { |
| 138 | + if nsRRset := nni.GetRRSet(dns.TypeNS); nsRRset != nil { |
| 139 | + return false, signNode(nni, opt, dnskeys, generator, nni == z.GetRootNode(), true) |
| 140 | + } |
| 141 | + } |
| 142 | + return auth, signNode(nni, opt, dnskeys, generator, nni == z.GetRootNode(), auth) |
| 143 | + }, true) |
| 144 | +} |
| 145 | + |
| 146 | +func signNode(nni NameNodeInterface, opt SignOption, dnskeys []*DNSKEY, generator Generator, apex, auth bool) error { |
| 147 | + if !auth { |
| 148 | + return nil |
| 149 | + } |
| 150 | + rrsig, err := GetRRSetOrCreate(nni, dns.TypeRRSIG, 0, generator) |
| 151 | + if err != nil { |
| 152 | + return err |
| 153 | + } |
| 154 | + err = nni.IterateNameRRSet(func(ri RRSetInterface) error { |
| 155 | + if ri.GetRRtype() == dns.TypeNS && !apex { |
| 156 | + return nil |
| 157 | + } |
| 158 | + rrsigRRs, err := SignRRSet(ri, opt, dnskeys) |
| 159 | + if err != nil { |
| 160 | + return err |
| 161 | + } |
| 162 | + for _, rr := range rrsigRRs { |
| 163 | + rrsig.AddRR(rr) |
| 164 | + } |
| 165 | + return nil |
| 166 | + }) |
| 167 | + if err != nil { |
| 168 | + return fmt.Errorf("failed to sign rrset: %w", err) |
| 169 | + } |
| 170 | + if len(rrsig.GetRRs()) == 0 { |
| 171 | + return nil |
| 172 | + } |
| 173 | + return nni.SetRRSet(rrsig) |
| 174 | +} |
| 175 | + |
| 176 | +func SignRRSet(ri RRSetInterface, opt SignOption, dnskeys []*DNSKEY) ([]*dns.RRSIG, error) { |
| 177 | + var rrs []*dns.RRSIG |
| 178 | + for _, dnskey := range dnskeys { |
| 179 | + if (ri.GetRRtype() == dns.TypeDNSKEY && dnskey.IsKSK()) || |
| 180 | + (ri.GetRRtype() != dns.TypeDNSKEY && dnskey.IsZSK()) { |
| 181 | + rrsig := &dns.RRSIG{ |
| 182 | + Hdr: dns.RR_Header{ |
| 183 | + Ttl: ri.GetTTL(), |
| 184 | + }, |
| 185 | + KeyTag: dnskey.GetRR().KeyTag(), |
| 186 | + SignerName: dnskey.GetRR().Header().Name, |
| 187 | + Algorithm: dnskey.GetRR().Algorithm, |
| 188 | + Inception: opt.GetInception(), |
| 189 | + Expiration: opt.GetExpiration(), |
| 190 | + } |
| 191 | + |
| 192 | + if err := rrsig.Sign(dnskey.GetSigner(), ri.GetRRs()); err != nil { |
| 193 | + return nil, fmt.Errorf("failed to sign: %w", err) |
| 194 | + } |
| 195 | + rrs = append(rrs, rrsig) |
| 196 | + } |
| 197 | + } |
| 198 | + return rrs, nil |
| 199 | +} |
| 200 | + |
| 201 | +func CreateDoE(z ZoneInterface, opt SignOption, generator RRSetGenerator) error { |
| 202 | + if generator == nil { |
| 203 | + generator = &DefaultGenerator{} |
| 204 | + } |
| 205 | + switch opt.DoEMethod { |
| 206 | + case DenialOfExistenceMethodNSEC, "": |
| 207 | + return createNSEC(z, generator) |
| 208 | + } |
| 209 | + return fmt.Errorf("not support: %s", opt.DoEMethod) |
| 210 | +} |
| 211 | + |
| 212 | +func createNSEC(z ZoneInterface, generator RRSetGenerator) error { |
| 213 | + var nodes = map[string]NameNodeInterface{} |
| 214 | + var names []string |
| 215 | + soa, err := GetSOA(z) |
| 216 | + if err != nil { |
| 217 | + return ErrBadZone |
| 218 | + } |
| 219 | + |
| 220 | + zoneCuts, _, err := GetZoneCuts(z.GetRootNode()) |
| 221 | + if err != nil { |
| 222 | + return ErrBadZone |
| 223 | + } |
| 224 | + |
| 225 | + // get next domain names |
| 226 | + z.GetRootNode().IterateNameNode(func(nni NameNodeInterface) error { |
| 227 | + // Blocks with no types present MUST NOT be included |
| 228 | + if nni.RRSetLen() == 0 { |
| 229 | + return nil |
| 230 | + } |
| 231 | + // A zone MUST NOT include an NSEC RR for any domain name that only holds glue records |
| 232 | + parent, strict := zoneCuts.GetNameNode(nni.GetName()) |
| 233 | + if parent.GetName() != z.GetName() { |
| 234 | + if !strict && parent.GetRRSet(dns.TypeNS) != nil { |
| 235 | + return nil |
| 236 | + } |
| 237 | + } |
| 238 | + nodes[nni.GetName()] = nni |
| 239 | + names = append(names, nni.GetName()) |
| 240 | + return nil |
| 241 | + }) |
| 242 | + |
| 243 | + sortedNames, _ := SortNames(names) |
| 244 | + for i, name := range sortedNames { |
| 245 | + nsec := &dns.NSEC{ |
| 246 | + Hdr: dns.RR_Header{ |
| 247 | + Name: name, |
| 248 | + Rrtype: dns.TypeNSEC, |
| 249 | + Class: dns.ClassINET, |
| 250 | + // The NSEC RR SHOULD have the same TTL value as the SOA minimum TTL field. |
| 251 | + // This is in the spirit of negative caching ([RFC2308]). |
| 252 | + Ttl: soa.Minttl, |
| 253 | + }, |
| 254 | + TypeBitMap: []uint16{dns.TypeRRSIG, dns.TypeNSEC}, |
| 255 | + } |
| 256 | + if i+1 < len(sortedNames) { |
| 257 | + nsec.NextDomain = sortedNames[i+1] |
| 258 | + } else { |
| 259 | + nsec.NextDomain = sortedNames[0] |
| 260 | + } |
| 261 | + rresetMap := nodes[name].CopyRRSetMap() |
| 262 | + for rtype := range rresetMap { |
| 263 | + switch rtype { |
| 264 | + case dns.TypeRRSIG: |
| 265 | + case dns.TypeNSEC: |
| 266 | + default: |
| 267 | + nsec.TypeBitMap = append(nsec.TypeBitMap, rtype) |
| 268 | + } |
| 269 | + } |
| 270 | + sort.SliceStable(nsec.TypeBitMap, func(i, j int) bool { return nsec.TypeBitMap[i] < nsec.TypeBitMap[j] }) |
| 271 | + |
| 272 | + set, err := generator.NewRRSet(name, soa.Minttl, dns.ClassINET, dns.TypeNSEC) |
| 273 | + if err != nil { |
| 274 | + return err |
| 275 | + } |
| 276 | + if err := set.AddRR(nsec); err != nil { |
| 277 | + return err |
| 278 | + } |
| 279 | + if err := nodes[name].SetRRSet(set); err != nil { |
| 280 | + return err |
| 281 | + } |
| 282 | + } |
| 283 | + return nil |
| 284 | +} |
0 commit comments