From 8a475d6114adda1a8654b07d437eb547a20234fa Mon Sep 17 00:00:00 2001 From: WeebDataHoarder <57538841+WeebDataHoarder@users.noreply.github.com> Date: Sat, 11 Jun 2022 14:38:03 +0200 Subject: [PATCH] Initial commit --- .gitignore | 1 + LICENSE | 19 ++++++++ api.go | 63 ++++++++++++++++++++++++ go.mod | 3 ++ identity.go | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 222 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 api.go create mode 100644 go.mod create mode 100644 identity.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..757fee3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e1f7fbe --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022 WeebDataHoarder, givna.me Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/api.go b/api.go new file mode 100644 index 0000000..086c854 --- /dev/null +++ b/api.go @@ -0,0 +1,63 @@ +package dns_api + +import ( + "crypto/ed25519" + "net/http" + "net/url" + "time" +) + +type Client struct { + privateKey ed25519.PrivateKey + client *http.Client + RequestExpirationTime time.Duration +} + +//NewClient Creates an API client with either a specified http Client, or nil +func NewClient(privateKey ed25519.PrivateKey, client *http.Client) *Client { + if client == nil { + client = http.DefaultClient + } + return &Client{ + privateKey: privateKey, + client: client, + } +} + +func (c *Client) Do(req *http.Request) (*http.Response, error) { + requestUrl, err := url.Parse(req.URL.String()) + if err != nil { + return nil, err + } + requestUrl.Host = req.Host + + var expirationTime time.Time + if c.RequestExpirationTime != 0 { + expirationTime = time.Now().Add(c.RequestExpirationTime) + } + + newUrl, err := CreateSignatureMessage(req.Method, requestUrl, c.privateKey, expirationTime) + if err != nil { + return nil, err + } + + //TODO: WithContext? + return c.client.Do(&http.Request{ + Method: req.Method, + URL: newUrl, + Header: req.Header, + Body: req.Body, + GetBody: req.GetBody, + ContentLength: req.ContentLength, + TransferEncoding: req.TransferEncoding, + Close: req.Close, + Host: req.Host, + Form: req.Form, + PostForm: req.PostForm, + MultipartForm: req.MultipartForm, + Trailer: req.Trailer, + Response: req.Response, + }) +} + +//TODO: server call methods diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dff88cb --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.gammaspectra.live/givna.me/dns-api + +go 1.18 diff --git a/identity.go b/identity.go new file mode 100644 index 0000000..370c8b8 --- /dev/null +++ b/identity.go @@ -0,0 +1,136 @@ +package dns_api + +import ( + "bytes" + "crypto/ed25519" + "encoding/hex" + "errors" + "fmt" + "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, requestUrl *url.URL) (ed25519.PublicKey, error) { + buf, publicKey, expirationTime, err := BuildSignatureMessage(method, 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, 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 := make([]byte, ed25519.PublicKeySize) + copy(publicKey, privateKey[ed25519.PublicKeySize:]) + + newRequestUrl.Query().Set(KeyPublicKey, hex.EncodeToString(publicKey)) + newRequestUrl.Query().Set(KeyExpiration, strconv.FormatInt(expirationTime.UTC().Unix(), 10)) + buf, publicKeyCheck, expirationTimeCheck, err := BuildSignatureMessage(method, 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, 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(requestUrl.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 +}