Initial commit
This commit is contained in:
commit
8a475d6114
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/.idea
|
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -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.
|
63
api.go
Normal file
63
api.go
Normal file
|
@ -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
|
136
identity.go
Normal file
136
identity.go
Normal file
|
@ -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
|
||||
}
|
Loading…
Reference in a new issue