package dns_api import ( "bytes" "encoding/hex" "errors" "fmt" "git.gammaspectra.live/givna.me/dns-api/ed25519" "net/url" "sort" "strconv" "strings" "time" ) var fieldSeparator = []byte{'\xff', '\xff', '\xff', '\xff'} const ( ExpirationTimeNever = 0 KeyPublicKey = "k" KeyExpiration = "x" KeySignature = "s" ) func VerifySignatureMessage(method string, host string, requestUrl *url.URL) (ed25519.PublicKey, error) { buf, publicKey, expirationTime, err := BuildSignatureMessage(method, host, requestUrl) if err != nil { return nil, err } signature, err := hex.DecodeString(requestUrl.Query().Get(KeySignature)) if err != nil { return nil, err } if len(signature) != ed25519.SignatureSize { return nil, fmt.Errorf("invalid signature size: expected %d, got %d", ed25519.SignatureSize, len(signature)) } if expirationTime != ExpirationTimeNever { checkTime := time.Now().UTC().Unix() if expirationTime < checkTime { return nil, fmt.Errorf("expiration verification failed: now %d, expiration %d", checkTime, expirationTime) } } if ed25519.Verify(publicKey, buf, signature) { return publicKey, nil } else { return nil, fmt.Errorf("signature verification failed") } } func CreateSignatureMessage(method string, host string, requestUrl *url.URL, privateKey ed25519.PrivateKey, expirationTime time.Time) (*url.URL, error) { newRequestUrl, err := url.Parse(requestUrl.String()) if err != nil { return nil, err } publicKey := privateKey.Public().(ed25519.PublicKey) newRequestUrl.Query().Set(KeyPublicKey, hex.EncodeToString(publicKey)) newRequestUrl.Query().Set(KeyExpiration, strconv.FormatInt(expirationTime.UTC().Unix(), 10)) buf, publicKeyCheck, expirationTimeCheck, err := BuildSignatureMessage(method, host, newRequestUrl) if err != nil { return nil, err } if bytes.Compare(publicKey, publicKeyCheck) != 0 { return nil, errors.New("public keys do not match") } if expirationTime.UTC().Unix() != expirationTimeCheck { return nil, errors.New("expiration times do not match") } signature := ed25519.Sign(privateKey, buf) newRequestUrl.Query().Set(KeySignature, hex.EncodeToString(signature)) return newRequestUrl, nil } func BuildSignatureMessage(method string, host string, requestUrl *url.URL) ([]byte, ed25519.PublicKey, int64, error) { var buf []byte buf = append(buf, []byte(strings.ToUpper(method))...) buf = append(buf, fieldSeparator...) publicKey, err := hex.DecodeString(requestUrl.Query().Get(KeyPublicKey)) if err != nil { return nil, nil, 0, err } if len(requestUrl.Query()[KeyPublicKey]) > 1 { return nil, nil, 0, errors.New("more than one public key given") } if len(publicKey) != ed25519.PublicKeySize { return nil, nil, 0, fmt.Errorf("invalid public key size: expected %d, got %d", ed25519.PublicKeySize, len(publicKey)) } expiry, err := strconv.ParseInt(requestUrl.Query().Get(KeyExpiration), 10, 0) if err != nil { return nil, nil, 0, err } if len(requestUrl.Query()[KeyExpiration]) > 1 { return nil, nil, 0, errors.New("more than one expiration given") } if len(requestUrl.Query()[KeySignature]) > 1 { return nil, nil, 0, errors.New("more than one signature given") } buf = append(buf, []byte(strings.Split(host, ":")[0])...) buf = append(buf, fieldSeparator...) buf = append(buf, []byte(requestUrl.Path)...) buf = append(buf, fieldSeparator...) var keys []string for k, v := range requestUrl.Query() { if k == KeyPublicKey || k == KeySignature { continue } for _, value := range v { keys = append(keys, url.QueryEscape(k)+"="+url.QueryEscape(value)) } } sort.Strings(keys) for _, v := range keys { buf = append(buf, []byte(v)...) buf = append(buf, fieldSeparator...) } return buf, publicKey, expiry, nil }