From 631cf39f549f32bfb610fea57b1f39fd3f5a0eaa Mon Sep 17 00:00:00 2001 From: Brendan McMillion Date: Mon, 25 Mar 2019 18:00:36 -0700 Subject: [PATCH 1/3] Implement DNSSEC resolver that can export proofs. License: MIT Signed-off-by: Brendan McMillion --- namesys/dnssec/cache/cache.go | 209 +++++++++++++++++++++ namesys/dnssec/dnssec.go | 160 +++++++++++++++++ namesys/dnssec/pb/result.pb.go | 141 +++++++++++++++ namesys/dnssec/pb/result.proto | 20 +++ namesys/dnssec/resolver.go | 310 ++++++++++++++++++++++++++++++++ namesys/dnssec/resolver_test.go | 25 +++ namesys/dnssec/result.go | 274 ++++++++++++++++++++++++++++ 7 files changed, 1139 insertions(+) create mode 100644 namesys/dnssec/cache/cache.go create mode 100644 namesys/dnssec/dnssec.go create mode 100644 namesys/dnssec/pb/result.pb.go create mode 100644 namesys/dnssec/pb/result.proto create mode 100644 namesys/dnssec/resolver.go create mode 100644 namesys/dnssec/resolver_test.go create mode 100644 namesys/dnssec/result.go diff --git a/namesys/dnssec/cache/cache.go b/namesys/dnssec/cache/cache.go new file mode 100644 index 00000000000..3529c23c100 --- /dev/null +++ b/namesys/dnssec/cache/cache.go @@ -0,0 +1,209 @@ +// Package cache implements a capped-size in-memory cache that randomly evicts +// elements when it reaches max size. +package cache + +import ( + "crypto/rand" + "runtime" + "sync" + "time" +) + +type Item struct { + Object interface{} + Expiration int64 +} + +// Returns true if the item has expired. +func (item Item) Expired() bool { + if item.Expiration == 0 { + return false + } + return time.Now().UnixNano() > item.Expiration +} + +const ( + // For use with functions that take an expiration time. + NoExpiration time.Duration = -1 + // For use with functions that take an expiration time. Equivalent to + // passing in the same expiration duration as was given to New() or + // NewFrom() when the cache was created (e.g. 5 minutes.) + DefaultExpiration time.Duration = 0 +) + +type Cache struct { + *cache + // If this is confusing, see the comment at the bottom of New() +} + +type cache struct { + defaultExpiration time.Duration + items map[string]Item + keys *keyList + mu sync.RWMutex + janitor *janitor +} + +// Add an item to the cache, replacing any existing item. If the duration is 0 +// (DefaultExpiration), the cache's default expiration time is used. If it is -1 +// (NoExpiration), the item never expires. +func (c *cache) Set(k string, x interface{}, d time.Duration) { + var e int64 + if d == DefaultExpiration { + d = c.defaultExpiration + } + if d > 0 { + e = time.Now().Add(d).UnixNano() + } + c.mu.Lock() + if _, ok := c.items[k]; !ok { + evicted, ok := c.keys.insert(k) + if ok { + delete(c.items, evicted) + } + } + c.items[k] = Item{ + Object: x, + Expiration: e, + } + c.mu.Unlock() +} + +// Get an item from the cache. Returns the item or nil, and a bool indicating +// whether the key was found. +func (c *cache) Get(k string) (interface{}, bool) { + c.mu.RLock() + item, found := c.items[k] + c.mu.RUnlock() + if !found { + return nil, false + } else if item.Expired() { + return nil, false + } + return item.Object, true +} + +// Delete all expired items from the cache. +func (c *cache) DeleteExpired() { + c.mu.Lock() + for i := 0; i < len(c.keys.keys); i++ { + k := c.keys.keys[i] + v, ok := c.items[k] + if !ok { + panic("cache inconsistent") + } + if v.Expired() { + c.keys.evictAt(i) + delete(c.items, k) + } + } + c.mu.Unlock() +} + +type janitor struct { + Interval time.Duration + stop chan bool +} + +func (j *janitor) Run(c *cache) { + ticker := time.NewTicker(j.Interval) + for { + select { + case <-ticker.C: + c.DeleteExpired() + case <-j.stop: + ticker.Stop() + return + } + } +} + +func stopJanitor(c *Cache) { + c.janitor.stop <- true +} + +func runJanitor(c *cache, ci time.Duration) { + j := &janitor{ + Interval: ci, + stop: make(chan bool), + } + c.janitor = j + go j.Run(c) +} + +func newCache(de time.Duration, maxSize int, m map[string]Item) *cache { + if de == 0 { + de = -1 + } + c := &cache{ + defaultExpiration: de, + items: m, + keys: &keyList{maxSize: maxSize}, + } + return c +} + +func newCacheWithJanitor(de time.Duration, ci time.Duration, maxSize int, m map[string]Item) *Cache { + c := newCache(de, maxSize, m) + // This trick ensures that the janitor goroutine (which--granted it + // was enabled--is running DeleteExpired on c forever) does not keep + // the returned C object from being garbage collected. When it is + // garbage collected, the finalizer stops the janitor goroutine, after + // which c can be collected. + C := &Cache{c} + if ci > 0 { + runJanitor(c, ci) + runtime.SetFinalizer(C, stopJanitor) + } + return C +} + +// New returns a new cache with a given default expiration duration and cleanup +// interval. If the expiration duration is less than one (or NoExpiration), the +// items in the cache never expire (by default), and must be deleted manually. +// If the cleanup interval is less than one, expired items are not deleted from +// the cache before calling c.DeleteExpired(). +func New(defaultExpiration, cleanupInterval time.Duration, maxSize int) *Cache { + items := make(map[string]Item) + return newCacheWithJanitor(defaultExpiration, cleanupInterval, maxSize, items) +} + +// keyList stores the list of keys in our cache in a way that is easy to +// randomly sample. +type keyList struct { + keys []string + maxSize int +} + +func (kl *keyList) insert(key string) (string, bool) { + if len(kl.keys) < kl.maxSize { + kl.keys = append(kl.keys, key) + return "", false + } + + // Randomly sample an index in keys. + buff := make([]byte, 8) + if _, err := rand.Read(buff); err != nil { + panic(err) + } + var i int + for _, b := range buff { + i = (i << 8) | int(b) + } + if i < 0 { + i = -i + } + i = i % len(kl.keys) + + // Replace the key at position i with the new one, return what was there. + old := kl.keys[i] + kl.keys[i] = key + return old, true +} + +func (kl *keyList) evictAt(i int) { + n := len(kl.keys) + + kl.keys[i], kl.keys[n-1] = kl.keys[n-1], kl.keys[i] + kl.keys = kl.keys[:n-1] +} diff --git a/namesys/dnssec/dnssec.go b/namesys/dnssec/dnssec.go new file mode 100644 index 00000000000..ffea2faded6 --- /dev/null +++ b/namesys/dnssec/dnssec.go @@ -0,0 +1,160 @@ +package dnssec + +import ( + "fmt" + "strings" + "time" + + "github.com/miekg/dns" +) + +func supportedAlg(alg uint8) bool { + return alg == 8 || alg == 13 +} + +// chooseKeyset takes the response from a DNSKEY query `msg` and returns the +// DNSKEY RRs from inside, along with the preferred signature over the RRs. +// +// The preferred signature is guaranteed to verify against a public key +// referenced by one of the DS RRs in `digests`. +func chooseKeyset(digests []*dns.DS, msg *dns.Msg) ([]*dns.DNSKEY, *dns.RRSIG, error) { + keys := make([]*dns.DNSKEY, 0) + for _, rr := range msg.Answer { + key, ok := rr.(*dns.DNSKEY) + if !ok { + continue + } + keys = append(keys, key) + } + + for _, rr := range msg.Answer { + cand, ok := rr.(*dns.RRSIG) + if !ok { + continue + } else if !supportedAlg(cand.Algorithm) { + continue + } else if err := verifyKeyset(digests, keys, cand); err != nil { + continue + } + + return keys, cand, nil + } + + return nil, nil, fmt.Errorf("no suitable signatures over the authority's keyset were found") +} + +// verifyKeyset returns an error unless there is a public key in `keys` that +// `sig` will verify against, and that this public key is referenced by a DS RR +// in `digests`. +func verifyKeyset(digests []*dns.DS, keys []*dns.DNSKEY, sig *dns.RRSIG) (err error) { + if !sig.ValidityPeriod(time.Time{}) { + return fmt.Errorf("signature is not currently valid") + } + + recs := make([]dns.RR, 0, len(keys)) + for _, rr := range keys { + recs = append(recs, rr) + } + + for _, key := range keys { + if key.Flags&dns.ZONE == 0 { + continue + } + cand := key.ToDS(dns.SHA256) + + for _, ds := range digests { + matches := ds.KeyTag == cand.KeyTag && + ds.Algorithm == cand.Algorithm && + ds.DigestType == cand.DigestType && + ds.Digest == cand.Digest && + strings.ToLower(ds.Hdr.Name) == strings.ToLower(key.Hdr.Name) + if !matches { + continue + } + err = sig.Verify(key, recs) + if err != nil { + continue + } + return nil + } + } + + if err == nil { + err = fmt.Errorf("no suitable signatures over the authority's keyset were found") + } + return err +} + +// chooseRecs takes a query response `msg` and returns the non-signature RRs +// from inside, along with the preferred signature over the RRs. The preferred +// signature verifies against one of the public keys in `keys`. +func chooseRecs(keys []*dns.DNSKEY, msg *dns.Msg) ([]dns.RR, *dns.RRSIG, error) { + recs := make([]dns.RR, 0) + for _, rr := range msg.Answer { + if _, ok := rr.(*dns.RRSIG); ok { + continue + } + recs = append(recs, rr) + } + + for _, rr := range msg.Answer { + sig, ok := rr.(*dns.RRSIG) + if !ok { + continue + } else if !supportedAlg(sig.Algorithm) { + continue + } else if err := verifyRecs(keys, recs, sig); err != nil { + continue + } + + return recs, sig, nil + } + + return nil, nil, fmt.Errorf("no suitable signatures over the resource record set were found") +} + +// verifyRecs returns an error unless there is a public key in `keys` that `sig` +// will verify against. +func verifyRecs(keys []*dns.DNSKEY, recs []dns.RR, sig *dns.RRSIG) (err error) { + if !sig.ValidityPeriod(time.Time{}) { + return fmt.Errorf("signature is not currently valid") + } + + for _, key := range keys { + if key.Flags&dns.ZONE == 0 { + continue + } else if !suffixed(recs, key.Hdr.Name) { + continue + } + err = sig.Verify(key, recs) + if err != nil { + continue + } + return nil + } + + if err == nil { + err = fmt.Errorf("no suitable signatures over the resource record set were found") + } + return err +} + +// suffixed returns true if every resource record in `recs` is a child of the +// given parent zone. +func suffixed(recs []dns.RR, parent string) bool { + parent = strings.ToLower(parent) + + suffix := parent + if suffix != "." { + suffix = "." + suffix + } + + for _, rr := range recs { + name := strings.ToLower(rr.Header().Name) + if name != parent && !strings.HasSuffix(name, suffix) { + return false + } + } + + return true +} diff --git a/namesys/dnssec/pb/result.pb.go b/namesys/dnssec/pb/result.pb.go new file mode 100644 index 00000000000..74f00ef99f0 --- /dev/null +++ b/namesys/dnssec/pb/result.pb.go @@ -0,0 +1,141 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: pb/result.proto + +/* +Package pb is a generated protocol buffer package. + +It is generated from these files: + pb/result.proto + +It has these top-level messages: + Delegation + Result +*/ +package pb + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type Delegation struct { + Keys [][]byte `protobuf:"bytes,1,rep,name=Keys,proto3" json:"Keys,omitempty"` + Digests [][]byte `protobuf:"bytes,2,rep,name=Digests,proto3" json:"Digests,omitempty"` + KeySig []byte `protobuf:"bytes,3,opt,name=KeySig,proto3" json:"KeySig,omitempty"` + DigestSig []byte `protobuf:"bytes,4,opt,name=DigestSig,proto3" json:"DigestSig,omitempty"` +} + +func (m *Delegation) Reset() { *m = Delegation{} } +func (m *Delegation) String() string { return proto.CompactTextString(m) } +func (*Delegation) ProtoMessage() {} +func (*Delegation) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +func (m *Delegation) GetKeys() [][]byte { + if m != nil { + return m.Keys + } + return nil +} + +func (m *Delegation) GetDigests() [][]byte { + if m != nil { + return m.Digests + } + return nil +} + +func (m *Delegation) GetKeySig() []byte { + if m != nil { + return m.KeySig + } + return nil +} + +func (m *Delegation) GetDigestSig() []byte { + if m != nil { + return m.DigestSig + } + return nil +} + +type Result struct { + Delegations []*Delegation `protobuf:"bytes,1,rep,name=Delegations" json:"Delegations,omitempty"` + Keys [][]byte `protobuf:"bytes,2,rep,name=Keys,proto3" json:"Keys,omitempty"` + Data [][]byte `protobuf:"bytes,3,rep,name=Data,proto3" json:"Data,omitempty"` + KeySig []byte `protobuf:"bytes,4,opt,name=KeySig,proto3" json:"KeySig,omitempty"` + DataSig []byte `protobuf:"bytes,5,opt,name=DataSig,proto3" json:"DataSig,omitempty"` +} + +func (m *Result) Reset() { *m = Result{} } +func (m *Result) String() string { return proto.CompactTextString(m) } +func (*Result) ProtoMessage() {} +func (*Result) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } + +func (m *Result) GetDelegations() []*Delegation { + if m != nil { + return m.Delegations + } + return nil +} + +func (m *Result) GetKeys() [][]byte { + if m != nil { + return m.Keys + } + return nil +} + +func (m *Result) GetData() [][]byte { + if m != nil { + return m.Data + } + return nil +} + +func (m *Result) GetKeySig() []byte { + if m != nil { + return m.KeySig + } + return nil +} + +func (m *Result) GetDataSig() []byte { + if m != nil { + return m.DataSig + } + return nil +} + +func init() { + proto.RegisterType((*Delegation)(nil), "pb.Delegation") + proto.RegisterType((*Result)(nil), "pb.Result") +} + +func init() { proto.RegisterFile("pb/result.proto", fileDescriptor0) } + +var fileDescriptor0 = []byte{ + // 194 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2f, 0x48, 0xd2, 0x2f, + 0x4a, 0x2d, 0x2e, 0xcd, 0x29, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x2a, 0x48, 0x52, + 0x2a, 0xe0, 0xe2, 0x72, 0x49, 0xcd, 0x49, 0x4d, 0x4f, 0x2c, 0xc9, 0xcc, 0xcf, 0x13, 0x12, 0xe2, + 0x62, 0xf1, 0x4e, 0xad, 0x2c, 0x96, 0x60, 0x54, 0x60, 0xd6, 0xe0, 0x09, 0x02, 0xb3, 0x85, 0x24, + 0xb8, 0xd8, 0x5d, 0x32, 0xd3, 0x53, 0x8b, 0x4b, 0x8a, 0x25, 0x98, 0xc0, 0xc2, 0x30, 0xae, 0x90, + 0x18, 0x17, 0x9b, 0x77, 0x6a, 0x65, 0x70, 0x66, 0xba, 0x04, 0xb3, 0x02, 0xa3, 0x06, 0x4f, 0x10, + 0x94, 0x27, 0x24, 0xc3, 0xc5, 0x09, 0x51, 0x02, 0x92, 0x62, 0x01, 0x4b, 0x21, 0x04, 0x94, 0xa6, + 0x30, 0x72, 0xb1, 0x05, 0x81, 0x9d, 0x21, 0x64, 0xc0, 0xc5, 0x8d, 0xb0, 0x1c, 0x62, 0x2b, 0xb7, + 0x11, 0x9f, 0x5e, 0x41, 0x92, 0x1e, 0x42, 0x38, 0x08, 0x59, 0x09, 0xdc, 0x81, 0x4c, 0x48, 0x0e, + 0x14, 0xe2, 0x62, 0x71, 0x49, 0x2c, 0x49, 0x94, 0x60, 0x86, 0x88, 0x81, 0xd8, 0x48, 0x4e, 0x63, + 0x41, 0x71, 0x1a, 0xc8, 0x33, 0x89, 0x25, 0x89, 0x20, 0x09, 0x56, 0xb0, 0x04, 0x8c, 0x9b, 0xc4, + 0x06, 0x0e, 0x13, 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0x46, 0xe5, 0xba, 0x24, 0x26, 0x01, + 0x00, 0x00, +} diff --git a/namesys/dnssec/pb/result.proto b/namesys/dnssec/pb/result.proto new file mode 100644 index 00000000000..28e5f157088 --- /dev/null +++ b/namesys/dnssec/pb/result.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; +package pb; + +message Delegation { + repeated bytes Keys = 1; + repeated bytes Digests = 2; + + bytes KeySig = 3; + bytes DigestSig = 4; +} + +message Result { + repeated Delegation Delegations = 1; + + repeated bytes Keys = 2; + repeated bytes Data = 3; + + bytes KeySig = 4; + bytes DataSig = 5; +} diff --git a/namesys/dnssec/resolver.go b/namesys/dnssec/resolver.go new file mode 100644 index 00000000000..c56f63da2fc --- /dev/null +++ b/namesys/dnssec/resolver.go @@ -0,0 +1,310 @@ +// Package dnssec implements a DNSSEC-validating resolver that's capable of +// exporting DNSSEC proofs. +package dnssec + +import ( + "context" + "fmt" + "net" + "time" + + "github.com/ipfs/go-ipfs/namesys/dnssec/cache" + + "github.com/miekg/dns" +) + +// rootDigests contains identifiers for the current root key-signing keys. +var rootDigests = []*dns.DS{ + &dns.DS{ + Hdr: dns.RR_Header{ + Name: ".", + Rrtype: 0x2b, + Class: 0x01, + Ttl: 0x1ed8, + Rdlength: 0x00, + }, + KeyTag: 0x4a5c, + Algorithm: 0x08, + DigestType: 0x02, + Digest: "49aac11d7b6f6446702e54a1607371607a1a41855200fd2ce1cdde32f24e8fb5", + }, + &dns.DS{ + Hdr: dns.RR_Header{ + Name: ".", + Rrtype: 0x2b, + Class: 0x01, + Ttl: 0x1ed8, + Rdlength: 0x00, + }, + KeyTag: 0x4f66, + Algorithm: 0x08, + DigestType: 0x02, + Digest: "e06d44b80b8f1d39a95c0b0d7c65d08458e880409bbc683457104237c7f8ec8d", + }, +} + +type cacheEntry struct { + msg *dns.Msg + signers []string +} + +type Resolver struct { + Cache *cache.Cache +} + +func (r *Resolver) LookupA(ctx context.Context, name string) ([]string, *Result, error) { + res, err := r.lookup(ctx, dns.Fqdn(name), dns.TypeA) + if err != nil { + return nil, nil, err + } + addrs, err := res.A(name) + if err != nil { + return nil, nil, err + } + return addrs, res, nil +} + +func (r *Resolver) LookupAAAA(ctx context.Context, name string) ([]string, *Result, error) { + res, err := r.lookup(ctx, dns.Fqdn(name), dns.TypeAAAA) + if err != nil { + return nil, nil, err + } + addrs, err := res.AAAA(name) + if err != nil { + return nil, nil, err + } + return addrs, res, nil +} + +func (r *Resolver) LookupTXT(ctx context.Context, name string) ([]string, *Result, error) { + res, err := r.lookup(ctx, dns.Fqdn(name), dns.TypeTXT) + if err != nil { + return nil, nil, err + } + txts, err := res.TXT(name) + if err != nil { + return nil, nil, err + } + return txts, res, nil +} + +// lookup performs the query and outputs the result along with a DNSSEC proof +// that this result is correct. +func (r *Resolver) lookup(ctx context.Context, name string, qtype uint16) (*Result, error) { + conn, err := r.connect(ctx) + if err != nil { + return nil, err + } + defer conn.Close() + + q := &query{ + cache: r.Cache, + conn: conn, + } + return q.lookup(name, qtype) +} + +// connect establishes a reliable connection to a recursive resolver. The +// resolver is expected to do all of the actual heavy DNS lifting. +func (r *Resolver) connect(ctx context.Context) (*dns.Conn, error) { + client := &dns.Client{ + Net: "tcp", + Dialer: &net.Dialer{ + Timeout: 2 * time.Second, + Cancel: ctx.Done(), + }, + } + conn, err := client.Dial("1dot1dot1dot1.cloudflare-dns.com:53") + if err != nil { + return nil, err + } + + deadline := time.Now().Add(10 * time.Second) + if sooner, ok := ctx.Deadline(); ok && sooner.Before(deadline) { + conn.SetDeadline(sooner) + } else { + conn.SetDeadline(deadline) + } + + return conn, nil +} + +type query struct { + cache *cache.Cache + conn *dns.Conn + + steps int + keys *dns.Msg + res *dns.Msg +} + +func (q *query) lookup(name string, qtype uint16) (*Result, error) { + // Get the data the client asked for. + res, signers, err := q.exchangeOneC(name, qtype) + if err != nil { + return nil, fmt.Errorf("failed to get response: %v", err) + } + q.steps = 0 + q.res = res + + // Foreach candidate signer, fetch their keyset and try to build a + // chain-of-trust to the root zone that authenticates the response. + for _, signer := range signers { + keys, _, err := q.exchangeOneC(signer, dns.TypeDNSKEY) + if err != nil { + return nil, fmt.Errorf("failed to get signer's keyset: %v", err) + } + q.keys = keys + + var res *Result + res, err = q.authenticate(signer, nil) + if err == nil { + return res, nil + } + } + + return nil, err +} + +// authenticate is a recursive method that builds a chain-of-trust from signer's +// DS record, up to the root zone. +// +// A DS record may have multiple signers, so we do a depth-first search and +// return the first chain that validates. +func (q *query) authenticate(signer string, delegs []delegMsg) (*Result, error) { + if signer == "." { + return newResult(reverseDelegs(delegs), q.keys, q.res) + } + const maxSteps = 10 + if q.steps >= maxSteps { + return nil, fmt.Errorf("tracing the chain of authority took too long") + } + q.steps += 1 + + deleg, authorities, err := q.exchangeOneC(signer, dns.TypeDS) + if err != nil { + return nil, fmt.Errorf("failed to find delegation: %v", err) + } + + for _, auth := range authorities { + authKeys, _, err := q.exchangeOneC(auth, dns.TypeDNSKEY) + if err != nil { + err = fmt.Errorf("failed to get authority's keyset: %v", err) + continue + } + delegs = append(delegs, delegMsg{authKeys, deleg}) + + var res *Result + res, err = q.authenticate(auth, delegs) + if err == nil { + return res, nil + } + + delegs = delegs[:len(delegs)-1] + } + + return nil, err +} + +// exchangeOneC is a caching wrapper around exchangeOne. +func (q *query) exchangeOneC(name string, qtype uint16) (*dns.Msg, []string, error) { + if q.cache == nil { + return q.exchangeOne(name, qtype) + } + cacheKey := fmt.Sprintf("%v:%v", name, qtype) + + res, ok := q.cache.Get(cacheKey) + if ok { + entry := res.(cacheEntry) + return entry.msg.Copy(), copySlice(entry.signers), nil + } + + msg, signers, err := q.exchangeOne(name, qtype) + if err != nil { + return nil, nil, err + } + q.cache.Set(cacheKey, cacheEntry{msg, signers}, cache.DefaultExpiration) + + return msg.Copy(), copySlice(signers), nil +} + +// exchangeOne sends a question to the resolver at `conn` and reads the +// response. It checks that the response is well-formed and signed (the +// signature is not verified). It returns the resolver's response and the +// de-duplicated names of the signers. +func (q *query) exchangeOne(name string, qtype uint16) (*dns.Msg, []string, error) { + req := new(dns.Msg) + req.SetQuestion(name, qtype) + req.SetEdns0(4096, true) // Tell the nameserver we support DNSSEC. + + err := q.conn.WriteMsg(req) + if err != nil { + return nil, nil, err + } + res, err := q.conn.ReadMsg() + if err != nil { + return nil, nil, err + } else if res.Id != req.Id { + return nil, nil, dns.ErrId + } else if res.Rcode != dns.RcodeSuccess { + return nil, nil, fmt.Errorf("unexpected response code (%v)", res.Rcode) + } else if len(res.Ns) > 0 { + return nil, nil, fmt.Errorf("response has unexpected records in authority section (Is TXT record with _dnslink. prefix set?)") + } + + // Verify that the response we got back has: some of the records of the type + // we asked for, a signature over those records, and nothing else. + var signers []string + hasResp, hasSig := false, false + + for _, rr := range res.Answer { + if sig, ok := rr.(*dns.RRSIG); ok { + // This is a signature record; store the signer name, if it's not a + // duplicate. + found := false + for _, cand := range signers { + if sig.SignerName == cand { + found = true + } + } + if !found { + signers = append(signers, sig.SignerName) + } + hasSig = true + continue + } + + hdr := rr.Header() + if hdr.Rrtype != qtype { + return nil, nil, fmt.Errorf("response has unexpected record type: %T (%v)", rr, hdr.Rrtype) + } + hasResp = true + } + + if !hasResp { + return nil, nil, fmt.Errorf("response has no records of the requested type (%v)", qtype) + } else if !hasSig { + return nil, nil, fmt.Errorf("response is not signed (Is DNSSEC configured?)") + } + return res, signers, nil +} + +func reverseDelegs(in []delegMsg) []delegMsg { + if in == nil { + return nil + } + out := make([]delegMsg, len(in)) + for i := 0; i < len(in); i++ { + out[i] = in[len(in)-i-1] + } + return out +} + +func copySlice(in []string) []string { + if in == nil { + return nil + } + out := make([]string, len(in)) + copy(out, in) + return out +} diff --git a/namesys/dnssec/resolver_test.go b/namesys/dnssec/resolver_test.go new file mode 100644 index 00000000000..d5a53b4cd0f --- /dev/null +++ b/namesys/dnssec/resolver_test.go @@ -0,0 +1,25 @@ +package dnssec + +import ( + "context" + "fmt" +) + +func ExampleResolver_LookupTXT() { + r := &Resolver{} + + txts, res, err := r.LookupTXT(context.Background(), "dnssec.brendans.website") + if err != nil { + panic(err) + } + fmt.Println(txts) + fmt.Println(res.Verify()) + fmt.Println(res.TXT("dnssec.brendans.website")) + fmt.Println(res.TXT("wrong-zone.com")) + + // Output: + // [secure txt record] + // + // [secure txt record] + // [] unexpected record name: dnssec.brendans.website. +} diff --git a/namesys/dnssec/result.go b/namesys/dnssec/result.go new file mode 100644 index 00000000000..e61b6583ec5 --- /dev/null +++ b/namesys/dnssec/result.go @@ -0,0 +1,274 @@ +package dnssec + +import ( + "fmt" + "strings" + + "github.com/golang/protobuf/proto" + "github.com/miekg/dns" + + "github.com/ipfs/go-ipfs/namesys/dnssec/pb" +) + +//go:generate protoc --go_out=. pb/result.proto + +type delegMsg struct { + keys *dns.Msg // keys contains the signed DNSKEY records for the current authority. + digests *dns.Msg // digests contains the signed DS records for the next authority. +} + +// Result wraps the output of a DNS query with the cryptographic material +// necessary to verify the output's validity. +// +// This data is meant to be serialized and sent over the wire to a client that +// can't do secure DNS resolution. They can then parse it and call +// Result.Verify() to see that this is correct. To extract the response data, +// they'd call the relevant method with the zone name they expect records for. +type Result struct { + Delegations []Delegation + + Keys []*dns.DNSKEY + Data []dns.RR + + KeySig, DataSig *dns.RRSIG +} + +func newResult(delegMsgs []delegMsg, keyMsg, resMsg *dns.Msg) (*Result, error) { + delegs := make([]Delegation, 0, len(delegMsgs)) + digests := rootDigests + + for _, deleg := range delegMsgs { + d, err := newDelegation(digests, deleg) + if err != nil { + return nil, err + } + delegs = append(delegs, *d) + digests = d.Digests + } + + keys, keySig, err := chooseKeyset(digests, keyMsg) + if err != nil { + return nil, err + } + data, dataSig, err := chooseRecs(keys, resMsg) + if err != nil { + return nil, err + } + + return &Result{ + Delegations: delegs, + + Keys: keys, + Data: data, + + KeySig: keySig, + DataSig: dataSig, + }, nil +} + +func (r *Result) A(name string) ([]string, error) { + name = strings.ToLower(dns.Fqdn(name)) + out := make([]string, 0) + + for _, rr := range r.Data { + addr, ok := rr.(*dns.A) + if !ok { + return nil, fmt.Errorf("unexpected record type: %T", rr) + } else if strings.ToLower(addr.Hdr.Name) != name { + return nil, fmt.Errorf("unexpected record name: %v", addr.Hdr.Name) + } + out = append(out, fmt.Sprint(addr.A)) + } + + return out, nil +} + +func (r *Result) AAAA(name string) ([]string, error) { + name = strings.ToLower(dns.Fqdn(name)) + out := make([]string, 0) + + for _, rr := range r.Data { + addr, ok := rr.(*dns.AAAA) + if !ok { + return nil, fmt.Errorf("unexpected record type: %T", rr) + } else if strings.ToLower(addr.Hdr.Name) != name { + return nil, fmt.Errorf("unexpected record name: %v", addr.Hdr.Name) + } + out = append(out, fmt.Sprint(addr.AAAA)) + } + + return out, nil +} + +func (r *Result) TXT(name string) ([]string, error) { + name = strings.ToLower(dns.Fqdn(name)) + out := make([]string, 0) + + for _, rr := range r.Data { + txt, ok := rr.(*dns.TXT) + if !ok { + return nil, fmt.Errorf("unexpected record type: %T", rr) + } else if strings.ToLower(txt.Hdr.Name) != name { + return nil, fmt.Errorf("unexpected record name: %v", txt.Hdr.Name) + } + out = append(out, strings.Join(txt.Txt, "")) + } + + return out, nil +} + +func (r *Result) Verify() error { + digests := rootDigests + for _, deleg := range r.Delegations { + if err := verifyKeyset(digests, deleg.Keys, deleg.KeySig); err != nil { + return err + } + recs := make([]dns.RR, 0, len(deleg.Digests)) + for _, rr := range deleg.Digests { + recs = append(recs, rr) + } + if err := verifyRecs(deleg.Keys, recs, deleg.DigestSig); err != nil { + return err + } + digests = deleg.Digests + } + + if err := verifyKeyset(digests, r.Keys, r.KeySig); err != nil { + return err + } else if err := verifyRecs(r.Keys, r.Data, r.DataSig); err != nil { + return err + } + + return nil +} + +func (r *Result) MarshalBinary() ([]byte, error) { + out := &pb.Result{} + + for _, del := range r.Delegations { + raw, err := del.toPB() + if err != nil { + return nil, err + } + out.Delegations = append(out.Delegations, raw) + } + + for _, key := range r.Keys { + raw, err := packRR(key, r.KeySig) + if err != nil { + return nil, err + } + out.Keys = append(out.Keys, raw) + } + + for _, data := range r.Data { + raw, err := packRR(data, r.DataSig) + if err != nil { + return nil, err + } + out.Data = append(out.Data, raw) + } + + keySig, err := packRR(r.KeySig, r.KeySig) + if err != nil { + return nil, err + } + out.KeySig = keySig + + dataSig, err := packRR(r.DataSig, r.DataSig) + if err != nil { + return nil, err + } + out.DataSig = dataSig + + return proto.Marshal(out) +} + +// Delegation is evidence provided by one authority that they are delegating +// control of a zone to a lower authority. The lower authority may delegate +// again to an even lower authority, such that there's a chain of delegations +// starting at the root zone. +type Delegation struct { + Keys []*dns.DNSKEY + Digests []*dns.DS + + KeySig, DigestSig *dns.RRSIG +} + +func newDelegation(digests []*dns.DS, msgs delegMsg) (*Delegation, error) { + keys, keySig, err := chooseKeyset(digests, msgs.keys) + if err != nil { + return nil, err + } + recs, digestSig, err := chooseRecs(keys, msgs.digests) + if err != nil { + return nil, err + } + + ds := make([]*dns.DS, 0, len(recs)) + for _, rr := range recs { + ds = append(ds, rr.(*dns.DS)) + } + + return &Delegation{ + Keys: keys, + Digests: ds, + + KeySig: keySig, + DigestSig: digestSig, + }, nil +} + +func (d Delegation) toPB() (*pb.Delegation, error) { + out := &pb.Delegation{} + + for _, key := range d.Keys { + raw, err := packRR(key, d.KeySig) + if err != nil { + return nil, err + } + out.Keys = append(out.Keys, raw) + } + + for _, digest := range d.Digests { + raw, err := packRR(digest, d.DigestSig) + if err != nil { + return nil, err + } + out.Digests = append(out.Digests, raw) + } + + keySig, err := packRR(d.KeySig, d.KeySig) + if err != nil { + return nil, err + } + out.KeySig = keySig + + digestSig, err := packRR(d.DigestSig, d.DigestSig) + if err != nil { + return nil, err + } + out.DigestSig = digestSig + + return out, nil +} + +func packRR(rr dns.RR, sig *dns.RRSIG) ([]byte, error) { + // Do minimum sanitization that is necessary for the RRSIG to verify. + hdr := rr.Header() + hdr.Ttl = sig.OrigTtl + + labels := dns.SplitDomainName(hdr.Name) + if len(labels) > int(sig.Labels) { + hdr.Name = "*." + strings.Join(labels[len(labels)-int(sig.Labels):], ".") + "." + } + hdr.Name = strings.ToLower(hdr.Name) + + // Serialize RR. + raw := make([]byte, len(hdr.Name)+int(hdr.Rdlength)+12) + n, err := dns.PackRR(rr, raw, 0, nil, false) + if err != nil { + return nil, err + } + return raw[:n], nil +} From 48d6229f90c75825991b9685816ca503447a0468 Mon Sep 17 00:00:00 2001 From: Brendan McMillion Date: Mon, 25 Mar 2019 18:18:11 -0700 Subject: [PATCH 2/3] Implementation of gateway that can serve proofs of correct behavior. License: MIT Signed-off-by: Brendan McMillion --- core/coreapi/unixfs.go | 11 +++ core/corehttp/gateway_handler.go | 141 +++++++++++++++++++++++++++++++ namesys/base.go | 26 ++++-- namesys/cache.go | 22 ++--- namesys/dns.go | 54 +++++++++--- namesys/interface.go | 5 +- namesys/namesys.go | 31 +++---- namesys/namesys_test.go | 2 +- namesys/proquint.go | 2 +- namesys/routing.go | 8 +- 10 files changed, 253 insertions(+), 49 deletions(-) diff --git a/core/coreapi/unixfs.go b/core/coreapi/unixfs.go index e4fb0f2e250..ed82419727c 100644 --- a/core/coreapi/unixfs.go +++ b/core/coreapi/unixfs.go @@ -145,6 +145,17 @@ func (api *UnixfsAPI) Get(ctx context.Context, p coreiface.Path) (files.Node, er return unixfile.NewUnixfsFile(ctx, ses.dag, nd) } +func (api *UnixfsAPI) GetWithProof(ctx context.Context, p coreiface.Path) (coreiface.ProofReader, error) { + ses := api.core().getSession(ctx) + + nd, err := ses.ResolveNode(ctx, p) + if err != nil { + return nil, err + } + + return uio.NewDagReaderWithProof(ctx, nd, ses.dag) +} + // Ls returns the contents of an IPFS or IPNS object(s) at path p, with the format: // ` ` func (api *UnixfsAPI) Ls(ctx context.Context, p coreiface.Path, opts ...options.UnixfsLsOption) (<-chan coreiface.DirEntry, error) { diff --git a/core/corehttp/gateway_handler.go b/core/corehttp/gateway_handler.go index a62aee4cdac..5706cc4d953 100644 --- a/core/corehttp/gateway_handler.go +++ b/core/corehttp/gateway_handler.go @@ -5,11 +5,13 @@ import ( "errors" "fmt" "io" + "mime" "net/http" "net/url" gopath "path" "runtime/debug" "strings" + "sync" "time" "github.com/ipfs/go-ipfs/core" @@ -25,6 +27,7 @@ import ( "github.com/ipfs/go-path/resolver" ft "github.com/ipfs/go-unixfs" "github.com/ipfs/go-unixfs/importer" + uio "github.com/ipfs/go-unixfs/io" coreiface "github.com/ipfs/interface-go-ipfs-core" "github.com/libp2p/go-libp2p-routing" "github.com/multiformats/go-multibase" @@ -347,6 +350,144 @@ func (i *gatewayHandler) getOrHeadHandler(ctx context.Context, w http.ResponseWr } } +func (i *gatewayHandler) secureGetHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + urlPath := r.URL.Path + escapedURLPath := r.URL.EscapedPath() + + // If the gateway is behind a reverse proxy and mounted at a sub-path, + // the prefix header can be set to signal this sub-path. + // It will be prepended to links in directory listings and the index.html redirect. + prefix := "" + if prfx := r.Header.Get("X-Ipfs-Gateway-Prefix"); len(prfx) > 0 { + for _, p := range i.config.PathPrefixes { + if prfx == p || strings.HasPrefix(prfx, p+"/") { + prefix = prfx + break + } + } + } + + // IPNSHostnameOption might have constructed an IPNS path using the Host header. + // In this case, we need the original path for constructing redirects + // and links that match the requested URL. + // For example, http://example.net would become /ipns/example.net, and + // the redirects and links would end up as http://example.net/ipns/example.net + originalUrlPath := prefix + urlPath + if hdr := r.Header.Get("X-Ipns-Original-Path"); len(hdr) > 0 { + originalUrlPath = prefix + hdr + } + + parsedPath, err := coreiface.ParsePath(urlPath) + if err != nil { + webError(w, "invalid ipfs path", err, http.StatusBadRequest) + return + } + + // Resolve path to the final DAG node for the ETag + preamble := &proofBuffer{} + resolvedPath, err := i.api.ResolvePath(context.WithValue(ctx, "proxy-preamble", preamble), parsedPath) + if err == coreiface.ErrOffline && !i.node.IsOnline { + webError(w, "ipfs resolve -r "+escapedURLPath, err, http.StatusServiceUnavailable) + return + } else if err != nil { + webError(w, "ipfs resolve -r "+escapedURLPath, err, http.StatusNotFound) + return + } + + // Check etag send back to us + etag := "\"" + resolvedPath.Cid().String() + "\"" + if r.Header.Get("If-None-Match") == etag || r.Header.Get("If-None-Match") == "W/"+etag { + w.WriteHeader(http.StatusNotModified) + return + } + + pr, err := i.api.Unixfs().GetWithProof(ctx, resolvedPath) + if err == uio.ErrIsDir { + http.Redirect(w, r, gopath.Join(originalUrlPath, "index.html"), 302) + return + } else if err != nil { + webError(w, "ipfs cat "+escapedURLPath, err, http.StatusNotFound) + return + } + defer pr.Close() + + i.addUserHeaders(w) // ok, _now_ write user's headers. + w.Header().Set("X-IPFS-Path", urlPath) + w.Header().Set("Etag", etag) + + if strings.HasPrefix(urlPath, ipfsPathPrefix) { + w.Header().Set("Cache-Control", "public, max-age=29030400, immutable") + } + if contentType := mime.TypeByExtension(gopath.Ext(urlPath)); contentType != "" { + w.Header().Set("Content-Type", contentType) + } else { + w.Header().Set("Content-Type", "text/html") + } + + if err := copyChunks(w, preamble); err != nil { + log.Warningf("error writing response preamble: %v", err) + } else if err := copyChunks(w, pr); err != nil { + log.Warningf("error writing response body: %v", err) + } +} + +// proofBuffer is an in-memory implementation of ProofWriter and ProofReader for +// the gateway's preamble, where proofs of name resolution are provided. +type proofBuffer struct { + mu sync.Mutex + buff [][]byte +} + +func (pb *proofBuffer) WriteChunk(data []byte) error { + pb.mu.Lock() + defer pb.mu.Unlock() + + pb.buff = append(pb.buff, data) + return nil +} + +func (pb *proofBuffer) ReadChunk() ([]byte, error) { + pb.mu.Lock() + defer pb.mu.Unlock() + + if len(pb.buff) == 0 { + return nil, io.EOF + } + out := pb.buff[0] + pb.buff = pb.buff[1:] + return out, nil +} + +func (pb *proofBuffer) Close() error { + pb.mu.Lock() + defer pb.mu.Unlock() + + pb.buff = nil + return nil +} + +func copyChunks(w io.Writer, pr coreiface.ProofReader) error { + for { + chunk, err := pr.ReadChunk() + if err == io.EOF { + return nil + } else if err != nil { + return err + } + + l := make([]byte, 3) + l[0] = byte(len(chunk) >> 16) + l[1] = byte(len(chunk) >> 8) + l[2] = byte(len(chunk)) + + if _, err := w.Write(l); err != nil { + return err + } else if _, err := w.Write(chunk); err != nil { + return err + } + } +} + type sizeReadSeeker interface { Size() (int64, error) diff --git a/namesys/base.go b/namesys/base.go index 27cc38f8885..e5fd1bb0a60 100644 --- a/namesys/base.go +++ b/namesys/base.go @@ -6,17 +6,19 @@ import ( "time" path "github.com/ipfs/go-path" + coreiface "github.com/ipfs/interface-go-ipfs-core" opts "github.com/ipfs/interface-go-ipfs-core/options/namesys" ) type onceResult struct { value path.Path + proof [][]byte ttl time.Duration err error } type resolver interface { - resolveOnceAsync(ctx context.Context, name string, options opts.ResolveOpts) <-chan onceResult + resolveOnceAsync(ctx context.Context, name string, needsProof bool, options opts.ResolveOpts) <-chan onceResult } // resolve is a helper for implementing Resolver.ResolveN using resolveOnce. @@ -24,23 +26,32 @@ func resolve(ctx context.Context, r resolver, name string, options opts.ResolveO ctx, cancel := context.WithCancel(ctx) defer cancel() - err := ErrResolveFailed - var p path.Path + var ( + p path.Path + proof [][]byte + err = ErrResolveFailed + ) resCh := resolveAsync(ctx, r, name, options) for res := range resCh { - p, err = res.Path, res.Err + p, proof, err = res.Path, res.Proof, res.Err if err != nil { break } } + if pw, ok := ctx.Value("proxy-preamble").(coreiface.ProofWriter); ok { + for _, p := range proof { + pw.WriteChunk(p) + } + } return p, err } func resolveAsync(ctx context.Context, r resolver, name string, options opts.ResolveOpts) <-chan Result { - resCh := r.resolveOnceAsync(ctx, name, options) + _, needsProof := ctx.Value("proxy-preamble").(coreiface.ProofWriter) + resCh := r.resolveOnceAsync(ctx, name, needsProof, options) depth := options.Depth outCh := make(chan Result, 1) @@ -48,6 +59,7 @@ func resolveAsync(ctx context.Context, r resolver, name string, options opts.Res defer close(outCh) var subCh <-chan Result var cancelSub context.CancelFunc + var proofStub [][]byte defer func() { if cancelSub != nil { cancelSub() @@ -68,7 +80,7 @@ func resolveAsync(ctx context.Context, r resolver, name string, options opts.Res } log.Debugf("resolved %s to %s", name, res.value.String()) if !strings.HasPrefix(res.value.String(), ipnsPrefix) { - emitResult(ctx, outCh, Result{Path: res.value}) + emitResult(ctx, outCh, Result{Path: res.value, Proof: res.proof}) break } @@ -92,6 +104,7 @@ func resolveAsync(ctx context.Context, r resolver, name string, options opts.Res p := strings.TrimPrefix(res.value.String(), ipnsPrefix) subCh = resolveAsync(subCtx, r, p, subopts) + proofStub = res.proof case res, ok := <-subCh: if !ok { subCh = nil @@ -100,6 +113,7 @@ func resolveAsync(ctx context.Context, r resolver, name string, options opts.Res // We don't bother returning here in case of context timeout as there is // no good reason to do that, and we may still be able to emit a result + res.Proof = append(proofStub, res.Proof...) emitResult(ctx, outCh, res) case <-ctx.Done(): return diff --git a/namesys/cache.go b/namesys/cache.go index 4a5cb5113ae..47e090fa2c0 100644 --- a/namesys/cache.go +++ b/namesys/cache.go @@ -6,14 +6,14 @@ import ( path "github.com/ipfs/go-path" ) -func (ns *mpns) cacheGet(name string) (path.Path, bool) { +func (ns *mpns) cacheGet(name string) (path.Path, [][]byte, bool) { if ns.cache == nil { - return "", false + return "", nil, false } ientry, ok := ns.cache.Get(name) if !ok { - return "", false + return "", nil, false } entry, ok := ientry.(cacheEntry) @@ -23,25 +23,27 @@ func (ns *mpns) cacheGet(name string) (path.Path, bool) { } if time.Now().Before(entry.eol) { - return entry.val, true + return entry.val, entry.proof, true } ns.cache.Remove(name) - return "", false + return "", nil, false } -func (ns *mpns) cacheSet(name string, val path.Path, ttl time.Duration) { +func (ns *mpns) cacheSet(name string, val path.Path, proof [][]byte, ttl time.Duration) { if ns.cache == nil || ttl <= 0 { return } ns.cache.Add(name, cacheEntry{ - val: val, - eol: time.Now().Add(ttl), + val: val, + proof: proof, + eol: time.Now().Add(ttl), }) } type cacheEntry struct { - val path.Path - eol time.Time + val path.Path + proof [][]byte + eol time.Time } diff --git a/namesys/dns.go b/namesys/dns.go index 931edec0019..44e3848d818 100644 --- a/namesys/dns.go +++ b/namesys/dns.go @@ -5,7 +5,10 @@ import ( "errors" "net" "strings" + "time" + "github.com/ipfs/go-ipfs/namesys/dnssec" + dnscache "github.com/ipfs/go-ipfs/namesys/dnssec/cache" path "github.com/ipfs/go-path" opts "github.com/ipfs/interface-go-ipfs-core/options/namesys" isd "github.com/jbenet/go-is-domain" @@ -18,11 +21,17 @@ type DNSResolver struct { lookupTXT LookupTXTFunc // TODO: maybe some sort of caching? // cache would need a timeout + dnssecResolver *dnssec.Resolver } // NewDNSResolver constructs a name resolver using DNS TXT records. func NewDNSResolver() *DNSResolver { - return &DNSResolver{lookupTXT: net.LookupTXT} + return &DNSResolver{ + lookupTXT: net.LookupTXT, + dnssecResolver: &dnssec.Resolver{ + Cache: dnscache.New(120*time.Second, 60*time.Second, 4096), + }, + } } // Resolve implements Resolver. @@ -37,13 +46,14 @@ func (r *DNSResolver) ResolveAsync(ctx context.Context, name string, options ... type lookupRes struct { path path.Path + proof [][]byte error error } // resolveOnce implements resolver. // TXT records for a given domain name should contain a b58 // encoded multihash. -func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options opts.ResolveOpts) <-chan onceResult { +func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, needsProof bool, options opts.ResolveOpts) <-chan onceResult { var fqdn string out := make(chan onceResult, 1) segments := strings.SplitN(name, "/", 2) @@ -63,10 +73,10 @@ func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options } rootChan := make(chan lookupRes, 1) - go workDomain(r, fqdn, rootChan) + go workDomain(ctx, r, fqdn, needsProof, rootChan) subChan := make(chan lookupRes, 1) - go workDomain(r, "_dnslink."+fqdn, subChan) + go workDomain(ctx, r, "_dnslink."+fqdn, needsProof, subChan) appendPath := func(p path.Path) (path.Path, error) { if len(segments) > 1 { @@ -86,7 +96,7 @@ func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options } if subRes.error == nil { p, err := appendPath(subRes.path) - emitOnceResult(ctx, out, onceResult{value: p, err: err}) + emitOnceResult(ctx, out, onceResult{value: p, proof: subRes.proof, err: err}) return } case rootRes, ok := <-rootChan: @@ -96,7 +106,7 @@ func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options } if rootRes.error == nil { p, err := appendPath(rootRes.path) - emitOnceResult(ctx, out, onceResult{value: p, err: err}) + emitOnceResult(ctx, out, onceResult{value: p, proof: rootRes.proof, err: err}) } case <-ctx.Done(): return @@ -110,24 +120,44 @@ func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options return out } -func workDomain(r *DNSResolver, name string, res chan lookupRes) { +func workDomain(ctx context.Context, r *DNSResolver, name string, needsProof bool, res chan lookupRes) { defer close(res) - txt, err := r.lookupTXT(name) + var ( + txt []string + proof *dnssec.Result + err error + ) + if needsProof { + txt, proof, err = r.dnssecResolver.LookupTXT(ctx, name) + } else { + txt, err = r.lookupTXT(name) + } if err != nil { - // Error is != nil - res <- lookupRes{"", err} + res <- lookupRes{"", nil, err} return } + // Serialize proof, it one was computed + var rawProof []byte + if proof != nil { + rawProof, err = proof.MarshalBinary() + if err != nil { + res <- lookupRes{"", nil, err} + return + } + rawProof = append([]byte{0}, rawProof...) + } + + // Return first valid record for _, t := range txt { p, err := parseEntry(t) if err == nil { - res <- lookupRes{p, nil} + res <- lookupRes{p, [][]byte{rawProof}, nil} return } } - res <- lookupRes{"", ErrResolveFailed} + res <- lookupRes{"", nil, ErrResolveFailed} } func parseEntry(txt string) (path.Path, error) { diff --git a/namesys/interface.go b/namesys/interface.go index 4db95ab3ca1..f61f7016232 100644 --- a/namesys/interface.go +++ b/namesys/interface.go @@ -64,8 +64,9 @@ type NameSystem interface { // Result is the return type for Resolver.ResolveAsync. type Result struct { - Path path.Path - Err error + Path path.Path + Proof [][]byte + Err error } // Resolver is an object capable of resolving names. diff --git a/namesys/namesys.go b/namesys/namesys.go index 94d4989922d..d3ea6801fe4 100644 --- a/namesys/namesys.go +++ b/namesys/namesys.go @@ -67,13 +67,13 @@ func (ns *mpns) ResolveAsync(ctx context.Context, name string, options ...opts.R res := make(chan Result, 1) if strings.HasPrefix(name, "/ipfs/") { p, err := path.ParsePath(name) - res <- Result{p, err} + res <- Result{p, nil, err} return res } if !strings.HasPrefix(name, "/") { p, err := path.ParsePath("/ipfs/" + name) - res <- Result{p, err} + res <- Result{p, nil, err} return res } @@ -81,7 +81,7 @@ func (ns *mpns) ResolveAsync(ctx context.Context, name string, options ...opts.R } // resolveOnce implements resolver. -func (ns *mpns) resolveOnceAsync(ctx context.Context, name string, options opts.ResolveOpts) <-chan onceResult { +func (ns *mpns) resolveOnceAsync(ctx context.Context, name string, needsProof bool, options opts.ResolveOpts) <-chan onceResult { out := make(chan onceResult, 1) if !strings.HasPrefix(name, ipnsPrefix) { @@ -97,16 +97,16 @@ func (ns *mpns) resolveOnceAsync(ctx context.Context, name string, options opts. key := segments[2] - if p, ok := ns.cacheGet(key); ok { + if p, proof, ok := ns.cacheGet(key); ok && (!needsProof || proof != nil) { if len(segments) > 3 { var err error p, err = path.FromSegments("", strings.TrimRight(p.String(), "/"), segments[3]) if err != nil { - emitOnceResult(ctx, out, onceResult{value: p, err: err}) + emitOnceResult(ctx, out, onceResult{value: p, proof: proof, err: err}) } } - out <- onceResult{value: p} + out <- onceResult{value: p, proof: proof} close(out) return out } @@ -125,34 +125,35 @@ func (ns *mpns) resolveOnceAsync(ctx context.Context, name string, options opts. res = ns.proquintResolver } - resCh := res.resolveOnceAsync(ctx, key, options) - var best onceResult + resCh := res.resolveOnceAsync(ctx, key, needsProof, options) + var best *onceResult go func() { defer close(out) for { select { case res, ok := <-resCh: if !ok { - if best != (onceResult{}) { - ns.cacheSet(key, best.value, best.ttl) + if best != nil { + ns.cacheSet(key, best.value, best.proof, best.ttl) } return } if res.err == nil { - best = res + best = &onceResult{} + *best = res } - p := res.value + p, proof := res.value, res.proof // Attach rest of the path if len(segments) > 3 { var err error p, err = path.FromSegments("", strings.TrimRight(p.String(), "/"), segments[3]) if err != nil { - emitOnceResult(ctx, out, onceResult{value: p, ttl: res.ttl, err: err}) + emitOnceResult(ctx, out, onceResult{value: p, proof: proof, ttl: res.ttl, err: err}) } } - emitOnceResult(ctx, out, onceResult{value: p, ttl: res.ttl, err: res.err}) + emitOnceResult(ctx, out, onceResult{value: p, proof: proof, ttl: res.ttl, err: res.err}) case <-ctx.Done(): return } @@ -186,6 +187,6 @@ func (ns *mpns) PublishWithEOL(ctx context.Context, name ci.PrivKey, value path. if ttEol := eol.Sub(time.Now()); ttEol < ttl { ttl = ttEol } - ns.cacheSet(peer.IDB58Encode(id), value, ttl) + ns.cacheSet(peer.IDB58Encode(id), value, nil, ttl) return nil } diff --git a/namesys/namesys_test.go b/namesys/namesys_test.go index 09c5a39c20d..d76d3ca9ee1 100644 --- a/namesys/namesys_test.go +++ b/namesys/namesys_test.go @@ -36,7 +36,7 @@ func testResolution(t *testing.T, resolver Resolver, name string, depth uint, ex } } -func (r *mockResolver) resolveOnceAsync(ctx context.Context, name string, options opts.ResolveOpts) <-chan onceResult { +func (r *mockResolver) resolveOnceAsync(ctx context.Context, name string, needsProof bool, options opts.ResolveOpts) <-chan onceResult { p, err := path.ParsePath(r.entries[name]) out := make(chan onceResult, 1) out <- onceResult{value: p, err: err} diff --git a/namesys/proquint.go b/namesys/proquint.go index 63cb62a0476..750a5bbcd38 100644 --- a/namesys/proquint.go +++ b/namesys/proquint.go @@ -17,7 +17,7 @@ func (r *ProquintResolver) Resolve(ctx context.Context, name string, options ... } // resolveOnce implements resolver. Decodes the proquint string. -func (r *ProquintResolver) resolveOnceAsync(ctx context.Context, name string, options opts.ResolveOpts) <-chan onceResult { +func (r *ProquintResolver) resolveOnceAsync(ctx context.Context, name string, needsProof bool, options opts.ResolveOpts) <-chan onceResult { out := make(chan onceResult, 1) defer close(out) diff --git a/namesys/routing.go b/namesys/routing.go index d5813377541..9eba4717496 100644 --- a/namesys/routing.go +++ b/namesys/routing.go @@ -48,7 +48,7 @@ func (r *IpnsResolver) ResolveAsync(ctx context.Context, name string, options .. // resolveOnce implements resolver. Uses the IPFS routing system to // resolve SFS-like names. -func (r *IpnsResolver) resolveOnceAsync(ctx context.Context, name string, options opts.ResolveOpts) <-chan onceResult { +func (r *IpnsResolver) resolveOnceAsync(ctx context.Context, name string, needsProof bool, options opts.ResolveOpts) <-chan onceResult { out := make(chan onceResult, 1) log.Debugf("RoutingResolver resolving %s", name) cancel := func() {} @@ -150,7 +150,11 @@ func (r *IpnsResolver) resolveOnceAsync(ctx context.Context, name string, option return } - emitOnceResult(ctx, out, onceResult{value: p, ttl: ttl}) + proof := make([]byte, 1+len(val)) + proof[0] = 1 + copy(proof[1:], val) + + emitOnceResult(ctx, out, onceResult{value: p, proof: [][]byte{proof}, ttl: ttl}) case <-ctx.Done(): return } From ac3169d5e3f971cf40c41b98f03cf8cda4676a5e Mon Sep 17 00:00:00 2001 From: Brendan McMillion Date: Mon, 25 Mar 2019 18:47:16 -0700 Subject: [PATCH 3/3] Update vendored dependencies. License: MIT Signed-off-by: Brendan McMillion --- go.mod | 6 ++++-- go.sum | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 107face83db..d34d697135e 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/fatih/color v1.7.0 // indirect github.com/fsnotify/fsnotify v1.4.7 github.com/gogo/protobuf v1.2.1 + github.com/golang/protobuf v1.3.0 github.com/hashicorp/golang-lru v0.5.1 github.com/hsanjuan/go-libp2p-http v0.0.2 github.com/ipfs/dir-index-html v1.0.3 @@ -51,10 +52,10 @@ require ( github.com/ipfs/go-metrics-prometheus v0.0.2 github.com/ipfs/go-mfs v0.0.4 github.com/ipfs/go-path v0.0.3 - github.com/ipfs/go-unixfs v0.0.4 + github.com/ipfs/go-unixfs v0.0.5-0.20190326013806-f9e9b27cc2bb github.com/ipfs/go-verifcid v0.0.1 github.com/ipfs/hang-fds v0.0.1 - github.com/ipfs/interface-go-ipfs-core v0.0.5 + github.com/ipfs/interface-go-ipfs-core v0.0.6-0.20190326010311-c4e66131e60c github.com/ipfs/iptb v1.4.0 github.com/ipfs/iptb-plugins v0.0.2 github.com/jbenet/go-is-domain v1.0.2 @@ -91,6 +92,7 @@ require ( github.com/libp2p/go-testutil v0.0.1 github.com/mattn/go-runewidth v0.0.4 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/miekg/dns v1.1.4 github.com/mitchellh/go-homedir v1.1.0 github.com/mr-tron/base58 v1.1.0 github.com/multiformats/go-multiaddr v0.0.1 diff --git a/go.sum b/go.sum index e8180afd0bc..c5ba71ff8d5 100644 --- a/go.sum +++ b/go.sum @@ -204,6 +204,8 @@ github.com/ipfs/go-unixfs v0.0.3 h1:09koecZaoJVoYy6Wkd/vo1lyQ4AdgAe83eJylQ7gAZw= github.com/ipfs/go-unixfs v0.0.3/go.mod h1:FX/6aS/Xg95JRc6UMyiMdZeNn+N5VkD8/yfLNwKW0Ks= github.com/ipfs/go-unixfs v0.0.4 h1:IApzQ+SnY0tfjqM7aU2b80CFYLZNHvhLmEZDIWr4e/E= github.com/ipfs/go-unixfs v0.0.4/go.mod h1:eIo/p9ADu/MFOuyxzwU+Th8D6xoxU//r590vUpWyfz8= +github.com/ipfs/go-unixfs v0.0.5-0.20190326013806-f9e9b27cc2bb h1:0jADgQuAWDo2nyCg/oS82btTJ1FZuRo8sf0kdgSMU4k= +github.com/ipfs/go-unixfs v0.0.5-0.20190326013806-f9e9b27cc2bb/go.mod h1:3xZc6JK+AWFYDnuLWPL4PX7MccwW3pc09oG8QE58JOI= github.com/ipfs/go-verifcid v0.0.1 h1:m2HI7zIuR5TFyQ1b79Da5N9dnnCP1vcu2QqawmWlK2E= github.com/ipfs/go-verifcid v0.0.1/go.mod h1:5Hrva5KBeIog4A+UpqlaIU+DEstipcJYQQZc0g37pY0= github.com/ipfs/hang-fds v0.0.1 h1:KGAxiGtJPT3THVRNT6yxgpdFPeX4ZemUjENOt6NlOn4= @@ -214,6 +216,8 @@ github.com/ipfs/interface-go-ipfs-core v0.0.5-0.20190325175850-33e0648669fb h1:k github.com/ipfs/interface-go-ipfs-core v0.0.5-0.20190325175850-33e0648669fb/go.mod h1:VceUOYu+kPEy8Ev/gAhzXFTIfc/7xILKnL4fgZg8tZM= github.com/ipfs/interface-go-ipfs-core v0.0.5 h1:lePQnz+SqDupeDrVWtzEIjZlcYAbG8tJLrttQWRmGRg= github.com/ipfs/interface-go-ipfs-core v0.0.5/go.mod h1:VceUOYu+kPEy8Ev/gAhzXFTIfc/7xILKnL4fgZg8tZM= +github.com/ipfs/interface-go-ipfs-core v0.0.6-0.20190326010311-c4e66131e60c h1:caBRZYmSPNCrAoFjlg0pEQyVzsWEc2bY3lQddV7Hz7c= +github.com/ipfs/interface-go-ipfs-core v0.0.6-0.20190326010311-c4e66131e60c/go.mod h1:VceUOYu+kPEy8Ev/gAhzXFTIfc/7xILKnL4fgZg8tZM= github.com/ipfs/iptb v1.4.0 h1:YFYTrCkLMRwk/35IMyC6+yjoQSHTEcNcefBStLJzgvo= github.com/ipfs/iptb v1.4.0/go.mod h1:1rzHpCYtNp87/+hTxG5TfCVn/yMY3dKnLn8tBiMfdmg= github.com/ipfs/iptb-plugins v0.0.2 h1:JZp4h/+7f00dY4Epr8gzF+VqKITXmVGsZabvmZp7E9I=