package pkarr
import (
"bytes"
"errors"
"fmt"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/tmc/go-iroh/key"
"golang.org/x/net/dns/dnsmessage"
)
const (
maxDNSPacketSize = 1000
headerSize = 104
MaxBytes = headerSize + maxDNSPacketSize
)
var (
ErrPacketTooLarge = errors .New ("pkarr: DNS packet too large" )
ErrTooShort = errors .New ("pkarr: signed packet too short" )
ErrTooLarge = errors .New ("pkarr: signed packet too large" )
ErrSignature = errors .New ("pkarr: invalid signature" )
ErrDNS = errors .New ("pkarr: DNS decoding error" )
ErrInvalidKey = errors .New ("pkarr: invalid public key" )
)
type SignedPacket struct {
bytes []byte
}
func FromTxtStrings (sk key .SecretKey , name string , values []string , ttl uint32 ) (*SignedPacket , error ) {
pub := sk .Public ()
origin := pub .EndpointID ().Z32 ()
normalized := normalizeName (origin , name )
encoded , err := buildTxtPacket (normalized , values , ttl )
if err != nil {
return nil , fmt .Errorf ("%w: %v" , ErrDNS , err )
}
if len (encoded ) > maxDNSPacketSize {
return nil , fmt .Errorf ("%w: %d bytes (max %d)" , ErrPacketTooLarge , len (encoded ), maxDNSPacketSize )
}
ts := Now ()
sig := sk .Sign (signable (ts .Micros (), encoded ))
pubBytes := pub .Bytes ()
sigBytes := sig .Bytes ()
out := make ([]byte , 0 , headerSize +len (encoded ))
out = append (out , pubBytes [:]...)
out = append (out , sigBytes [:]...)
out = append (out , ts .beBytes ()...)
out = append (out , encoded ...)
return &SignedPacket {bytes : out }, nil
}
func FromBytes (b []byte ) (*SignedPacket , error ) {
if err := checkLen (b ); err != nil {
return nil , err
}
pub , err := key .PublicKeyFromSlice (b [:32 ])
if err != nil {
return nil , fmt .Errorf ("%w: %v" , ErrInvalidKey , err )
}
sig , err := key .SignatureFromSlice (b [32 :96 ])
if err != nil {
return nil , fmt .Errorf ("%w: %v" , ErrSignature , err )
}
var tsBytes [8 ]byte
copy (tsBytes [:], b [96 :104 ])
ts := timestampFromBE (tsBytes )
encoded := b [104 :]
if err := pub .Verify (signable (ts .Micros (), encoded ), sig ); err != nil {
return nil , fmt .Errorf ("%w: %v" , ErrSignature , err )
}
if _ , err := parsePacket (encoded ); err != nil {
return nil , fmt .Errorf ("%w: %v" , ErrDNS , err )
}
return &SignedPacket {bytes : bytes .Clone (b )}, nil
}
func FromBytesUnchecked (b []byte ) (*SignedPacket , error ) {
if err := checkLen (b ); err != nil {
return nil , err
}
if _ , err := parsePacket (b [104 :]); err != nil {
return nil , fmt .Errorf ("%w: %v" , ErrDNS , err )
}
return &SignedPacket {bytes : bytes .Clone (b )}, nil
}
func FromRelayPayload (pub key .PublicKey , payload []byte ) (*SignedPacket , error ) {
pubBytes := pub .Bytes ()
b := make ([]byte , 0 , 32 +len (payload ))
b = append (b , pubBytes [:]...)
b = append (b , payload ...)
return FromBytes (b )
}
func (p *SignedPacket ) Bytes () []byte { return p .bytes }
func (p *SignedPacket ) RelayPayload () []byte { return bytes .Clone (p .bytes [32 :]) }
func (p *SignedPacket ) PublicKey () key .PublicKey {
k , _ := key .PublicKeyFromSlice (p .bytes [:32 ])
return k
}
func (p *SignedPacket ) Signature () key .Signature {
s , _ := key .SignatureFromSlice (p .bytes [32 :96 ])
return s
}
func (p *SignedPacket ) Timestamp () Timestamp {
var b [8 ]byte
copy (b [:], p .bytes [96 :104 ])
return timestampFromBE (b )
}
func (p *SignedPacket ) EncodedPacket () []byte { return p .bytes [104 :] }
func (p *SignedPacket ) MoreRecentThan (other *SignedPacket ) bool {
if p .Timestamp () == other .Timestamp () {
return bytes .Compare (p .EncodedPacket (), other .EncodedPacket ()) > 0
}
return p .Timestamp ().Micros () > other .Timestamp ().Micros ()
}
func (p *SignedPacket ) TxtRecords (name string ) []string {
origin := p .PublicKey ().EndpointID ().Z32 ()
normalized := normalizeName (origin , name )
records , err := parsePacket (p .EncodedPacket ())
if err != nil {
return nil
}
var out []string
for _ , r := range records {
rrName := strings .TrimSuffix (r .name , "." )
if rrName == normalized {
out = append (out , r .txt )
} else if rel , ok := withoutZone (rrName , origin ); ok && rel == strings .TrimSuffix (name , "." ) {
out = append (out , r .txt )
}
}
return out
}
func (p *SignedPacket ) AllTxtRecords () [][2 ]string {
origin := p .PublicKey ().EndpointID ().Z32 ()
records , err := parsePacket (p .EncodedPacket ())
if err != nil {
return nil
}
var out [][2 ]string
for _ , r := range records {
rrName := strings .TrimSuffix (r .name , "." )
rel , _ := withoutZone (rrName , origin )
out = append (out , [2 ]string {rel , r .txt })
}
return out
}
func checkLen(b []byte ) error {
if len (b ) < headerSize {
return fmt .Errorf ("%w: %d bytes (min %d)" , ErrTooShort , len (b ), headerSize )
}
if len (b ) > MaxBytes {
return fmt .Errorf ("%w: %d bytes (max %d)" , ErrTooLarge , len (b ), MaxBytes )
}
return nil
}
func signable(timestamp uint64 , v []byte ) []byte {
prefix := "3:seqi" + strconv .FormatUint (timestamp , 10 ) + "e1:v" + strconv .Itoa (len (v )) + ":"
out := make ([]byte , 0 , len (prefix )+len (v ))
out = append (out , prefix ...)
out = append (out , v ...)
return out
}
func normalizeName(origin , name string ) string {
name = strings .TrimSuffix (name , "." )
parts := strings .Split (name , "." )
last := ""
if len (parts ) > 0 {
last = parts [len (parts )-1 ]
}
if last == origin {
return name
}
if last == "@" || last == "" {
return origin
}
return name + "." + origin
}
func withoutZone(name , origin string ) (string , bool ) {
if name == origin {
return "" , true
}
if suffix := "." + origin ; strings .HasSuffix (name , suffix ) {
return strings .TrimSuffix (name , suffix ), true
}
return name , false
}
type Timestamp uint64
var lastTimestamp atomic .Uint64
func Now () Timestamp {
micros := uint64 (time .Now ().UnixMicro ())
for {
last := lastTimestamp .Load ()
next := micros
if next <= last {
next = last + 1
}
if lastTimestamp .CompareAndSwap (last , next ) {
return Timestamp (next )
}
}
}
func TimestampFromMicros (micros uint64 ) Timestamp { return Timestamp (micros ) }
func (t Timestamp ) Micros () uint64 { return uint64 (t ) }
func (t Timestamp ) beBytes () []byte {
var b [8 ]byte
v := uint64 (t )
for i := 7 ; i >= 0 ; i -- {
b [i ] = byte (v )
v >>= 8
}
return b [:]
}
func timestampFromBE(b [8 ]byte ) Timestamp {
var v uint64
for _ , x := range b {
v = v <<8 | uint64 (x )
}
return Timestamp (v )
}
var _ = dnsmessage .TypeTXT
The pages are generated with Golds v0.8.4 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .