mirror of
https://github.com/kataras/iris.git
synced 2025-03-15 12:26:29 +01:00
implement the Iris Crypto Library for Request Authentication and Verification. With Examples and Tests.
Relative to this one as well: https://github.com/kataras/iris/issues/1200 Former-commit-id: 3a29e7398b7fdeb9b48a118b742d419d5681d56b
This commit is contained in:
parent
35389c6ef8
commit
9dbb300d9b
|
@ -365,6 +365,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her
|
||||||
- [OAUth2](authentication/oauth2/main.go)
|
- [OAUth2](authentication/oauth2/main.go)
|
||||||
- [JWT](experimental-handlers/jwt/main.go)
|
- [JWT](experimental-handlers/jwt/main.go)
|
||||||
- [Sessions](#sessions)
|
- [Sessions](#sessions)
|
||||||
|
- [Request Authentication](authentication/request/main.go) **NEW**
|
||||||
|
|
||||||
### File Server
|
### File Server
|
||||||
|
|
||||||
|
|
137
_examples/authentication/request/main.go
Normal file
137
_examples/authentication/request/main.go
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/kataras/iris"
|
||||||
|
"github.com/kataras/iris/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Change that to your owns, usally you have an ECDSA private key
|
||||||
|
// per identify, let's say a user, stored in a database
|
||||||
|
// or somewhere else and you use its public key
|
||||||
|
// to sign a user's payload and when this client
|
||||||
|
// wants to use this payload, on another route,
|
||||||
|
// you verify it comparing the signature of the payload
|
||||||
|
// with the user's public key.
|
||||||
|
//
|
||||||
|
// Use the crypto.MustGenerateKey to generate a random key
|
||||||
|
// or import
|
||||||
|
// the "github.com/kataras/iris/crypto/sign"
|
||||||
|
// and use its
|
||||||
|
// sign.ParsePrivateKey/ParsePublicKey(theKey []byte)
|
||||||
|
// to convert data or local file to an *ecdsa.PrivateKey.
|
||||||
|
testPrivateKey = crypto.MustGenerateKey()
|
||||||
|
testPublicKey = &testPrivateKey.PublicKey
|
||||||
|
)
|
||||||
|
|
||||||
|
type testPayloadStructure struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Iris crypto package offers
|
||||||
|
// authentication (with optional encryption in top of) and verification
|
||||||
|
// of raw []byte data with `crypto.Marshal/Unmarshal` functions
|
||||||
|
// and JSON payloads with `crypto.SignJSON/VerifyJSON functions.
|
||||||
|
//
|
||||||
|
// Let's use the `SignJSON` and `VerifyJSON` here as an example,
|
||||||
|
// as this is the most common scenario for a web application.
|
||||||
|
func main() {
|
||||||
|
app := iris.New()
|
||||||
|
|
||||||
|
app.Post("/auth/json", func(ctx iris.Context) {
|
||||||
|
ticket, err := crypto.SignJSON(testPrivateKey, ctx.Request().Body)
|
||||||
|
if err != nil {
|
||||||
|
ctx.StatusCode(iris.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send just the signature back
|
||||||
|
// ctx.WriteString(ticket.Signature)
|
||||||
|
// or the whole payload + the signature:
|
||||||
|
ctx.JSON(ticket)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Post("/verify/json", func(ctx iris.Context) {
|
||||||
|
var verificatedPayload testPayloadStructure // this can be anything.
|
||||||
|
|
||||||
|
// The VerifyJSON excepts the body to be a JSON structure of
|
||||||
|
// {
|
||||||
|
// "signature": the generated signature from /auth/json,
|
||||||
|
// "payload": the JSON client payload
|
||||||
|
// }
|
||||||
|
// That is the form of the `crypto.Ticket` structure.
|
||||||
|
//
|
||||||
|
// However, you are not limited to use that form, another common practise is to
|
||||||
|
// have the signature and the payload we need to check in the same string representation
|
||||||
|
// and for a better security you add encryption in top of it, so an outsider cannot understand what is what.
|
||||||
|
// Let's say that the signature can be optionally provided by a URL ENCODED parameter
|
||||||
|
// and the request body is the payload without any encryption
|
||||||
|
// -
|
||||||
|
// of course you can pass an GCM type of encryption/decryption as Marshal's and Unmarshal's last input argument,
|
||||||
|
// see more about this at the iris/crypto/gcm subpackage for ready-to-use solutions.
|
||||||
|
// -
|
||||||
|
// So we will check if a url parameter is given, if so we will combine the signature and the body into one slice of bytes
|
||||||
|
// and we will make use of the `crypto.Unmarshal` instead of the `crypto.VerifyJSON` function
|
||||||
|
// -
|
||||||
|
if signature := ctx.URLParam("signature"); signature != "" {
|
||||||
|
payload, err := ioutil.ReadAll(ctx.Request().Body)
|
||||||
|
if err != nil {
|
||||||
|
ctx.StatusCode(iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := append([]byte(signature), payload...)
|
||||||
|
|
||||||
|
originalPayloadBytes, ok := crypto.Unmarshal(testPublicKey, data, nil)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
ctx.Writef("this does not match, please try again\n")
|
||||||
|
ctx.StatusCode(iris.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.ContentType("application/json")
|
||||||
|
ctx.Write(originalPayloadBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := crypto.VerifyJSON(testPublicKey, ctx.Request().Body, &verificatedPayload)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Writef("error on verification: %v\n", err)
|
||||||
|
ctx.StatusCode(iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
ctx.Writef("this does not match, please try again\n")
|
||||||
|
ctx.StatusCode(iris.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give back the verificated payload or use it.
|
||||||
|
ctx.JSON(verificatedPayload)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 1.
|
||||||
|
// curl -X POST -H "Content-Type: application/json" -d '{"key": "this is a key", "value": "this is a value"}' http://localhost:8080/auth/json
|
||||||
|
// 2. The result will be something like this:
|
||||||
|
// {"payload":{"key":"this is a key","value":"this is a value"},"signature":"UgXgbXXvs9nAB3Pg0mG1WR0KBn2KpD/xBIsyOv1o4ZpzKs45hB/yxXiGN1k4Y+mgjdBxP6Gg26qajK6216pAGA=="}
|
||||||
|
// 3. Copy-paste the whole result and do:
|
||||||
|
// curl -X POST -H "Content-Type: application/json" -d '{"payload":{"key":"this is a key","value":"this is a value"},"signature":"UgXgbXXvs9nAB3Pg0mG1WR0KBn2KpD/xBIsyOv1o4ZpzKs45hB/yxXiGN1k4Y+mgjdBxP6Gg26qajK6216pAGA=="}' http://localhost:8080/verify/json
|
||||||
|
// 4. Or pass by ?signature encoded URL parameter:
|
||||||
|
// curl -X POST -H "Content-Type: application/json" -d '{"key": "this is a key", "value": "this is a value"}' http://localhost:8080/verify/json?signature=UgXgbXXvs9nAB3Pg0mG1WR0KBn2KpD%2FxBIsyOv1o4ZpzKs45hB%2FyxXiGN1k4Y%2BmgjdBxP6Gg26qajK6216pAGA%3D%3D
|
||||||
|
// 5. At both cases the result should be:
|
||||||
|
// {"key":"this is a key","value":"this is a value"}
|
||||||
|
// Otherise the verification failed.
|
||||||
|
//
|
||||||
|
// Note that each time server is restarted a new private and public key pair is generated,
|
||||||
|
// look at the start of the program.
|
||||||
|
app.Run(iris.Addr(":8080"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can read more examples and run testable code
|
||||||
|
// at the `iris/crypto`, `iris/crypto/sign`
|
||||||
|
// and `iris/crypto/gcm` packages themselves.
|
149
crypto/crypto.go
Normal file
149
crypto/crypto.go
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/crypto/gcm"
|
||||||
|
"github.com/kataras/iris/crypto/sign"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// MustGenerateKey generates an ecdsa public and private key pair.
|
||||||
|
// It panics if any error occurred.
|
||||||
|
MustGenerateKey = sign.MustGenerateKey
|
||||||
|
|
||||||
|
// MustGenerateAESKey generates an aes key.
|
||||||
|
// It panics if any error occurred.
|
||||||
|
MustGenerateAESKey = gcm.MustGenerateKey
|
||||||
|
// DefaultADATA is the default associated data used for `Encrypt` and `Decrypt`
|
||||||
|
// when "additionalData" is empty.
|
||||||
|
DefaultADATA = []byte("FFA0A43EA6B8C829AD403817B2F5B7A2")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encryption is the method signature when data should be signed and returned as encrypted.
|
||||||
|
type Encryption func(privateKey *ecdsa.PrivateKey, data []byte) ([]byte, error)
|
||||||
|
|
||||||
|
// Decryption is the method signature when data should be decrypted before signed.
|
||||||
|
type Decryption func(publicKey *ecdsa.PublicKey, data []byte) ([]byte, error)
|
||||||
|
|
||||||
|
// Encrypt returns an `Encryption` option to be used on `Marshal`.
|
||||||
|
// If "aesKey" is not empty then the "data" associated with the "additionalData" will be encrypted too.
|
||||||
|
// If "aesKey" is not empty but "additionalData" is, then the `DefaultADATA` will be used to encrypt "data".
|
||||||
|
// If "aesKey" is empty then encryption is disabled, the return value will be only signed.
|
||||||
|
//
|
||||||
|
// See `Unmarshal` and `Decrypt` too.
|
||||||
|
func Encrypt(aesKey, additionalData []byte) Encryption {
|
||||||
|
if len(aesKey) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(additionalData) == 0 {
|
||||||
|
additionalData = DefaultADATA
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(_ *ecdsa.PrivateKey, plaintext []byte) ([]byte, error) {
|
||||||
|
return gcm.Encrypt(aesKey, plaintext, additionalData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt returns an `Decryption` option to be used on `Unmarshal`.
|
||||||
|
// If "aesKey" is not empty then the result will be decrypted.
|
||||||
|
// If "aesKey" is not empty but "additionalData" is,
|
||||||
|
// then the `DefaultADATA` will be used to decrypt the encrypted "data".
|
||||||
|
// If "aesKey" is empty then decryption is disabled.
|
||||||
|
//
|
||||||
|
// If `Marshal` had an `Encryption` then `Unmarshal` must have also.
|
||||||
|
//
|
||||||
|
// See `Marshal` and `Encrypt` too.
|
||||||
|
func Decrypt(aesKey, additionalData []byte) Decryption {
|
||||||
|
if len(aesKey) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(additionalData) == 0 {
|
||||||
|
additionalData = DefaultADATA
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(_ *ecdsa.PublicKey, ciphertext []byte) ([]byte, error) {
|
||||||
|
return gcm.Decrypt(aesKey, ciphertext, additionalData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal signs and, optionally, encrypts the "data".
|
||||||
|
//
|
||||||
|
// The form of the output value is: signature_of_88_length followed by the raw_data_or_encrypted_data,
|
||||||
|
// i.e "R+eqxA3LslRif0KoxpevpNILAs4Kh4mccCCoE0sRjICkj9xy0/gsxeUd2wfcGK5mzIZ6tM3A939Wjif0xwZCog==7001f30..."
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Returns non-nil error if any error occurred.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// data, _ := ioutil.ReadAll(r.Body)
|
||||||
|
// signedData, err := crypto.Marshal(testPrivateKey, data, nil)
|
||||||
|
// w.Write(signedData)
|
||||||
|
// Or if data should be encrypted:
|
||||||
|
// signedEncryptedData, err := crypto.Marshal(testPrivateKey, data, crypto.Encrypt(aesKey, nil))
|
||||||
|
func Marshal(privateKey *ecdsa.PrivateKey, data []byte, encrypt Encryption) ([]byte, error) {
|
||||||
|
if encrypt != nil {
|
||||||
|
b, err := encrypt(privateKey, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data = b
|
||||||
|
}
|
||||||
|
|
||||||
|
// sign the encrypted data if "encrypt" exists.
|
||||||
|
sig, err := sign.Sign(privateKey, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, base64.StdEncoding.EncodedLen(len(sig)))
|
||||||
|
base64.StdEncoding.Encode(buf, sig)
|
||||||
|
|
||||||
|
return append(buf, data...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal verifies the "data" and, optionally, decrypts the output.
|
||||||
|
//
|
||||||
|
// Returns returns the signed raw data; without the signature and decrypted if "decrypt" is not nil.
|
||||||
|
// The second output value reports whether the verification and any decryption of the data succeed or not.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// data, _ := ioutil.ReadAll(ctx.Request().Body)
|
||||||
|
// verifiedPlainPayload, err := crypto.Unmarshal(ecdsaPublicKey, data, nil)
|
||||||
|
// ctx.Write(verifiedPlainPayload)
|
||||||
|
// Or if data are encrypted and they should be decrypted:
|
||||||
|
// verifiedDecryptedPayload, err := crypto.Unmarshal(ecdsaPublicKey, data, crypto.Decrypt(aesKey, nil))
|
||||||
|
func Unmarshal(publicKey *ecdsa.PublicKey, data []byte, decrypt Decryption) ([]byte, bool) {
|
||||||
|
if len(data) <= 90 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
sig, body := data[0:88], data[88:]
|
||||||
|
|
||||||
|
buf := make([]byte, base64.StdEncoding.DecodedLen(len(sig)))
|
||||||
|
n, err := base64.StdEncoding.Decode(buf, sig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
sig = buf[:n]
|
||||||
|
|
||||||
|
// verify the encrypted data as they are, the signature is linked with these.
|
||||||
|
ok, err := sign.Verify(publicKey, sig, body)
|
||||||
|
if !ok || err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to decrypt the body and finally return it as plain, its original form.
|
||||||
|
if decrypt != nil {
|
||||||
|
body, err = decrypt(publicKey, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, ok && err == nil
|
||||||
|
}
|
96
crypto/crypto_test.go
Normal file
96
crypto/crypto_test.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testPrivateKey = MustGenerateKey()
|
||||||
|
testPublicKey = &testPrivateKey.PublicKey
|
||||||
|
testAESKey = MustGenerateAESKey()
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMarshalAndUnmarshal(t *testing.T) {
|
||||||
|
testPayloadData := []byte(`{"mykey":"myvalue","mysecondkey@":"mysecondv#lu3@+!"}!+,==||any<data>[here]`)
|
||||||
|
|
||||||
|
signHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data, _ := ioutil.ReadAll(r.Body)
|
||||||
|
signedEncryptedPayload, err := Marshal(testPrivateKey, data, Encrypt(testAESKey, nil))
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(signedEncryptedPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
publicKey := testPublicKey
|
||||||
|
if r.URL.Path == "/verify/otherkey" {
|
||||||
|
// test with other, generated, public key.
|
||||||
|
publicKey = &MustGenerateKey().PublicKey
|
||||||
|
}
|
||||||
|
data, _ := ioutil.ReadAll(r.Body)
|
||||||
|
payload, ok := Unmarshal(publicKey, data, Decrypt(testAESKey, nil))
|
||||||
|
if !ok {
|
||||||
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-send the payload.
|
||||||
|
w.Write(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
testPayload := testPayloadData
|
||||||
|
t.Logf("signing: sending payload: %s", testPayload)
|
||||||
|
|
||||||
|
signRequest := httptest.NewRequest("POST", "/sign", bytes.NewBuffer(testPayload))
|
||||||
|
signRec := httptest.NewRecorder()
|
||||||
|
signHandler(signRec, signRequest)
|
||||||
|
|
||||||
|
gotSignedEncrypted, _ := ioutil.ReadAll(signRec.Body)
|
||||||
|
|
||||||
|
// Looks like this:
|
||||||
|
// jWQIL5gqTd1JqyHoTDXSaEtOmJdpYuzU0cyEn/9uDMW2JcPi4FkYfkkCfKyLFzlwhbykXsSJXOV11yVnS3EG4w==885c46964d92cce1fb36f9dfd76f2003000338e8605cd59fd0b5a84abf8175c41bf8bdbac0327cbc3cec17bf42ff9c
|
||||||
|
t.Logf("verification: sending signed encrypted payload:\n%s", gotSignedEncrypted)
|
||||||
|
verifyRequest := httptest.NewRequest("POST", "/verify", bytes.NewBuffer(gotSignedEncrypted))
|
||||||
|
verifyRec := httptest.NewRecorder()
|
||||||
|
verifyHandler(verifyRec, verifyRequest)
|
||||||
|
verifyRequest.Body.Close()
|
||||||
|
|
||||||
|
if expected, got := http.StatusOK, verifyRec.Code; expected != got {
|
||||||
|
t.Fatalf("verification: expected status code: %d but got: %d", expected, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotPayload, err := ioutil.ReadAll(verifyRec.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(testPayload, gotPayload) {
|
||||||
|
t.Fatalf("verification: expected payload: '%s' but got: '%s'", testPayload, gotPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("got plain payload:\n%s\n\n", gotPayload)
|
||||||
|
|
||||||
|
// test the same payload, with the same signature but with other public key (see handler checks the path for that).
|
||||||
|
t.Logf("verification: sending the same signed encrypted data which should not be verified due to a different key pair...")
|
||||||
|
verifyRequest = httptest.NewRequest("POST", "/verify/otherkey", bytes.NewBuffer(gotSignedEncrypted))
|
||||||
|
verifyRec = httptest.NewRecorder()
|
||||||
|
verifyHandler(verifyRec, verifyRequest)
|
||||||
|
verifyRequest.Body.Close()
|
||||||
|
|
||||||
|
if expected, got := http.StatusUnprocessableEntity, verifyRec.Code; expected != got {
|
||||||
|
t.Fatalf("verification: expected status code: %d but got: %d", expected, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotPayload, _ = ioutil.ReadAll(verifyRec.Body)
|
||||||
|
if len(gotPayload) > 0 {
|
||||||
|
t.Fatalf("verification should fail and no payload should return but got: '%s'", gotPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("correct, it didn't match")
|
||||||
|
}
|
134
crypto/gcm/gcm.go
Normal file
134
crypto/gcm/gcm.go
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
// Package gcm implements encryption/decription using the AES algorithm and the Galois/Counter Mode.
|
||||||
|
package gcm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MustGenerateKey generates an aes key.
|
||||||
|
// It panics if any error occurred.
|
||||||
|
func MustGenerateKey() []byte {
|
||||||
|
aesKey, err := GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return aesKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateKey returns a random aes key.
|
||||||
|
func GenerateKey() ([]byte, error) {
|
||||||
|
key := make([]byte, 64)
|
||||||
|
n, err := rand.Read(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return encode(key[:n]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt encrypts and authenticates the plain data and additional data
|
||||||
|
// and returns the ciphertext of it.
|
||||||
|
// It uses the AEAD cipher mode providing authenticated encryption with associated
|
||||||
|
// data.
|
||||||
|
// The same additional data must be kept the same for `Decrypt`.
|
||||||
|
func Encrypt(aesKey, data, additionalData []byte) ([]byte, error) {
|
||||||
|
key, err := decode(aesKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
h := sha512.New()
|
||||||
|
h.Write(key)
|
||||||
|
digest := encode(h.Sum(nil))
|
||||||
|
|
||||||
|
// key based on the hash itself, we have space because of sha512.
|
||||||
|
newKey, err := decode(digest[:64])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// nonce based on the hash itself.
|
||||||
|
nonce, err := decode(digest[64:(64 + 24)])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
aData, err := decode(additionalData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(newKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext := encode(gcm.Seal(nil, nonce, data, aData))
|
||||||
|
return ciphertext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts and authenticates ciphertext, authenticates the
|
||||||
|
// additional data and, if successful, returns the resulting plain data.
|
||||||
|
// The additional data must match the value passed to `Encrypt`.
|
||||||
|
func Decrypt(aesKey, ciphertext, additionalData []byte) ([]byte, error) {
|
||||||
|
key, err := decode(aesKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
h := sha512.New()
|
||||||
|
h.Write(key)
|
||||||
|
digest := encode(h.Sum(nil))
|
||||||
|
|
||||||
|
newKey, err := decode(digest[:64])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nonce, err := decode(digest[64:(64 + 24)])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
additionalData, err = decode(additionalData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(newKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext, err = decode(ciphertext)
|
||||||
|
return gcm.Open(nil, nonce, ciphertext, additionalData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(src []byte) ([]byte, error) {
|
||||||
|
buf := make([]byte, hex.DecodedLen(len(src)))
|
||||||
|
n, err := hex.Decode(buf, src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(src []byte) []byte {
|
||||||
|
buf := make([]byte, hex.EncodedLen(len(src)))
|
||||||
|
hex.Encode(buf, src)
|
||||||
|
return buf
|
||||||
|
}
|
46
crypto/gcm/gcm_test.go
Normal file
46
crypto/gcm/gcm_test.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package gcm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testKey = MustGenerateKey()
|
||||||
|
|
||||||
|
func TestEncryptDecrypt(t *testing.T) {
|
||||||
|
if len(testKey) == 0 {
|
||||||
|
t.Fatalf("testKey is empty??")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
payload []byte
|
||||||
|
aData []byte // IV of a random aes-256-cbc, 32 size.
|
||||||
|
}{
|
||||||
|
{[]byte("test my content 1"), []byte("FFA0A43EA6B8C829AD403817B2F5B7A2")},
|
||||||
|
{[]byte("test my content 2"), []byte("364787B9AF1AEE4BE26690EA8CBF4AB7")},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
ciphertext, err := Encrypt(testKey, tt.payload, tt.aData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("[%d] encrypt error: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := Decrypt(testKey, ciphertext, tt.aData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("[%d] decrypt error: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(payload, tt.payload) {
|
||||||
|
t.Fatalf("[%d] expected data to be decrypted to: '%s' but got: '%s'", i, tt.payload, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test with other, invalid key, should fail to decrypt.
|
||||||
|
tempKey := MustGenerateKey()
|
||||||
|
|
||||||
|
payload, err = Decrypt(tempKey, ciphertext, tt.aData)
|
||||||
|
if err == nil || len(payload) > 0 {
|
||||||
|
t.Fatalf("[%d] verification should fail but passed for '%s'", i, tt.payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
94
crypto/json.go
Normal file
94
crypto/json.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/crypto/sign"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ticket contains the original payload raw data
|
||||||
|
// and the generated signature.
|
||||||
|
//
|
||||||
|
// Look `SignJSON` and `VerifyJSON` for more details.
|
||||||
|
type Ticket struct {
|
||||||
|
Payload json.RawMessage `json:"payload"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignJSON signs the incoming JSON request payload based on
|
||||||
|
// client's "privateKey" and the "r" (could be ctx.Request().Body).
|
||||||
|
//
|
||||||
|
// It generates the signature and returns a structure called `Ticket`.
|
||||||
|
// The `Ticket` just contains the original client's payload raw data
|
||||||
|
// and the generated signature.
|
||||||
|
//
|
||||||
|
// Returns non-nil error if any error occurred.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// ticket, err := crypto.SignJSON(testPrivateKey, ctx.Request().Body)
|
||||||
|
// b, err := json.Marshal(ticket)
|
||||||
|
// ctx.Write(b)
|
||||||
|
func SignJSON(privateKey *ecdsa.PrivateKey, r io.Reader) (Ticket, error) {
|
||||||
|
data, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil || len(data) == 0 {
|
||||||
|
return Ticket{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sig, err := sign.Sign(privateKey, data)
|
||||||
|
if err != nil {
|
||||||
|
return Ticket{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ticket := Ticket{
|
||||||
|
Payload: data,
|
||||||
|
Signature: base64.StdEncoding.EncodeToString(sig),
|
||||||
|
}
|
||||||
|
return ticket, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyJSON verifies the incoming JSON request,
|
||||||
|
// by reading the "r" which should decodes to a `Ticket`.
|
||||||
|
// The `Ticket` is verified against the given "publicKey", the `Ticket#Signature` and
|
||||||
|
// `Ticket#Payload` data (original request's payload data which was signed by `SignPayload`).
|
||||||
|
//
|
||||||
|
// Returns true wether the verification succeed or not.
|
||||||
|
// The "toPayloadPtr" should be a pointer to a value of the same payload structure the client signed on.
|
||||||
|
// If and only if the verification succeed the payload value is filled from the `Ticket#Payload` raw data.
|
||||||
|
//
|
||||||
|
// Check for both output arguments in order to:
|
||||||
|
// 1. verification (true/false and error) and
|
||||||
|
// 2. ticket's original json payload parsed and "toPayloadPtr" is filled successfully (error).
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// var myPayload myJSONStruct
|
||||||
|
// ok, err := crypto.VerifyJSON(publicKey, ctx.Request().Body, &myPayload)
|
||||||
|
func VerifyJSON(publicKey *ecdsa.PublicKey, r io.Reader, toPayloadPtr interface{}) (bool, error) {
|
||||||
|
data, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ticket := new(Ticket)
|
||||||
|
err = json.Unmarshal(data, ticket)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sig, err := base64.StdEncoding.DecodeString(ticket.Signature)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := sign.Verify(publicKey, sig, ticket.Payload)
|
||||||
|
if ok && toPayloadPtr != nil {
|
||||||
|
// if and only if the verification succeed we
|
||||||
|
// set the payload to the structured/map value of "toPayloadPtr".
|
||||||
|
err = json.Unmarshal(ticket.Payload, toPayloadPtr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok, err
|
||||||
|
}
|
114
crypto/json_test.go
Normal file
114
crypto/json_test.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJSONSignAndVerify(t *testing.T) {
|
||||||
|
type testJSON struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
signHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ticket, err := SignJSON(testPrivateKey, r.Body)
|
||||||
|
if err != nil {
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422
|
||||||
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(ticket)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
w.Write(b)
|
||||||
|
// or
|
||||||
|
// fmt.Fprintf(w, "%s", ticket.Signature)
|
||||||
|
// to send just the signature.
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
publicKey := testPublicKey
|
||||||
|
if r.URL.Path == "/verify/otherkey" {
|
||||||
|
// test with other, generated, public key.
|
||||||
|
publicKey = &MustGenerateKey().PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload testJSON
|
||||||
|
ok, err := VerifyJSON(publicKey, r.Body, &payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
w.WriteHeader(http.StatusUnprocessableEntity) // or forbidden or unauthorized.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-send the payload.
|
||||||
|
b, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
w.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks like this:
|
||||||
|
// {"key":"mykey","value":"myvalue"}
|
||||||
|
testPayload := testJSON{"mykey", "myvalue"}
|
||||||
|
payload, _ := json.Marshal(testPayload)
|
||||||
|
t.Logf("signing: sending payload: %s", payload)
|
||||||
|
|
||||||
|
signRequest := httptest.NewRequest("POST", "/sign", bytes.NewBuffer(payload))
|
||||||
|
signRec := httptest.NewRecorder()
|
||||||
|
signHandler(signRec, signRequest)
|
||||||
|
|
||||||
|
gotTicketPayload, _ := ioutil.ReadAll(signRec.Body)
|
||||||
|
|
||||||
|
// Looks like this:
|
||||||
|
// {
|
||||||
|
// "signature": "D4PF6Hc0CrsO6MXAPxsLdhrVLKdmUOsN3Qm/Dr1y8yS80FQSgpU8Frr81fAJSKNwwW3dHhpoYvRi0t04MrukOQ==",
|
||||||
|
// "payload": {"key":"mykey","value":"myvalue"}
|
||||||
|
// }
|
||||||
|
t.Logf("verification: sending ticket: %s", gotTicketPayload)
|
||||||
|
verifyRequest := httptest.NewRequest("POST", "/verify", bytes.NewBuffer(gotTicketPayload))
|
||||||
|
verifyRec := httptest.NewRecorder()
|
||||||
|
verifyHandler(verifyRec, verifyRequest)
|
||||||
|
verifyRequest.Body.Close()
|
||||||
|
|
||||||
|
if expected, got := http.StatusOK, verifyRec.Code; expected != got {
|
||||||
|
t.Fatalf("verification: expected status code: %d but got: %d", expected, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotPayload, err := ioutil.ReadAll(verifyRec.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(payload, gotPayload) {
|
||||||
|
t.Fatalf("verification: expected payload: '%s' but got: '%s'", payload, gotTicketPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test the same payload, with the same signature but with other public key (see handler checks the path for that).
|
||||||
|
t.Logf("verification: sending the same ticket which should not be verified due to a different key pair...")
|
||||||
|
verifyRequest = httptest.NewRequest("POST", "/verify/otherkey", bytes.NewBuffer(gotTicketPayload))
|
||||||
|
verifyRec = httptest.NewRecorder()
|
||||||
|
verifyHandler(verifyRec, verifyRequest)
|
||||||
|
verifyRequest.Body.Close()
|
||||||
|
|
||||||
|
if expected, got := http.StatusUnprocessableEntity, verifyRec.Code; expected != got {
|
||||||
|
t.Fatalf("verification: expected status code: %d but got: %d", expected, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotPayload, _ = ioutil.ReadAll(verifyRec.Body)
|
||||||
|
if len(gotPayload) > 0 {
|
||||||
|
t.Fatalf("verification should fail and no payload should return but got: '%s'", gotPayload)
|
||||||
|
}
|
||||||
|
}
|
145
crypto/sign/sign.go
Normal file
145
crypto/sign/sign.go
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
// Package sign signs and verifies any format of data by
|
||||||
|
// using the ECDSA P-384 digital signature and authentication algorithm.
|
||||||
|
//
|
||||||
|
// https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm
|
||||||
|
// https://apps.nsa.gov/iaarchive/library/ia-guidance/ia-solutions-for-classified/algorithm-guidance/suite-b-implementers-guide-to-fips-186-3-ecdsa.cfm
|
||||||
|
// https://www.nsa.gov/Portals/70/documents/resources/everyone/csfc/csfc-faqs.pdf
|
||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509" // the key encoding.
|
||||||
|
"encoding/pem" // the data encoding format.
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
// the, modern, hash implementation,
|
||||||
|
// commonly used in popular crypto concurrencies too.
|
||||||
|
"golang.org/x/crypto/sha3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MustGenerateKey generates a public and private key pair.
|
||||||
|
// It panics if any error occurred.
|
||||||
|
func MustGenerateKey() *ecdsa.PrivateKey {
|
||||||
|
privateKey, err := GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return privateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateKey generates a public and private key pair.
|
||||||
|
func GenerateKey() (*ecdsa.PrivateKey, error) {
|
||||||
|
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePrivateKey generates a private key as pem text.
|
||||||
|
// It returns empty on any error.
|
||||||
|
func GeneratePrivateKey() string {
|
||||||
|
privateKey, err := GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKeyB, err := marshalPrivateKey(privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(privateKeyB)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign signs the "data" using the "privateKey".
|
||||||
|
// It returns the signature.
|
||||||
|
func Sign(privateKey *ecdsa.PrivateKey, data []byte) ([]byte, error) {
|
||||||
|
h := sha3.New256()
|
||||||
|
_, err := h.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
digest := h.Sum(nil)
|
||||||
|
|
||||||
|
r, s, err := ecdsa.Sign(rand.Reader, privateKey, digest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// sig := elliptic.Marshal(elliptic.P256(), r, s)
|
||||||
|
sig := append(r.Bytes(), s.Bytes()...)
|
||||||
|
|
||||||
|
return sig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify verifies the "data" in signature "sig" (96 length if 384) using the "publicKey".
|
||||||
|
// It reports whether the signature is valid or not.
|
||||||
|
func Verify(publicKey *ecdsa.PublicKey, sig, data []byte) (bool, error) {
|
||||||
|
h := sha3.New256()
|
||||||
|
_, err := h.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
digest := h.Sum(nil)
|
||||||
|
|
||||||
|
// 0:32 & 32:64 for 256, always because it's constant.
|
||||||
|
// 0:48 & 48:96 for 384 but it is not constant-time, so it's 96 or 97 length,
|
||||||
|
// also something like that elliptic.Unmarshal(elliptic.P384(), sig)
|
||||||
|
// doesn't work.
|
||||||
|
|
||||||
|
r := new(big.Int).SetBytes(sig[0:32])
|
||||||
|
s := new(big.Int).SetBytes(sig[32:64])
|
||||||
|
|
||||||
|
return ecdsa.Verify(publicKey, digest, r, s), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var errNotValidBlock = errors.New("invalid block")
|
||||||
|
|
||||||
|
// ParsePrivateKey accepts a pem x509-encoded private key and decodes to *ecdsa.PrivateKey.
|
||||||
|
func ParsePrivateKey(key []byte) (*ecdsa.PrivateKey, error) {
|
||||||
|
block, _ := pem.Decode(key)
|
||||||
|
if block == nil {
|
||||||
|
return nil, errNotValidBlock
|
||||||
|
}
|
||||||
|
return x509.ParseECPrivateKey(block.Bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePublicKey accepts a pem x509-encoded public key and decodes to *ecdsa.PrivateKey.
|
||||||
|
func ParsePublicKey(key []byte) (*ecdsa.PublicKey, error) {
|
||||||
|
block, _ := pem.Decode(key)
|
||||||
|
if block == nil {
|
||||||
|
return nil, errNotValidBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKeyV, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKey, ok := publicKeyV.(*ecdsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, errNotValidBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
return publicKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalPrivateKey(key *ecdsa.PrivateKey) ([]byte, error) {
|
||||||
|
privateKeyAnsDer, err := x509.MarshalECPrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyAnsDer}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalPublicKey(key *ecdsa.PublicKey) ([]byte, error) {
|
||||||
|
publicKeyAnsDer, err := x509.MarshalPKIXPublicKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: publicKeyAnsDer}), nil
|
||||||
|
}
|
74
crypto/sign/sign_test.go
Normal file
74
crypto/sign/sign_test.go
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testPrivateKey = MustGenerateKey()
|
||||||
|
testPublicKey = &testPrivateKey.PublicKey
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateKey(t *testing.T) {
|
||||||
|
privateKeyB, err := marshalPrivateKey(testPrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("private key: %v", err)
|
||||||
|
}
|
||||||
|
publicKeyB, err := marshalPublicKey(testPublicKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("public key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("%s", privateKeyB)
|
||||||
|
t.Logf("%s", publicKeyB)
|
||||||
|
|
||||||
|
privateKeyParsed, err := ParsePrivateKey(privateKeyB)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKeyParsed, err := ParsePublicKey(publicKeyB)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("public key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(testPrivateKey, privateKeyParsed) {
|
||||||
|
t.Fatalf("expected private key to be:\n%#+v\nbut got:\n%#+v", testPrivateKey, privateKeyParsed)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(testPublicKey, publicKeyParsed) {
|
||||||
|
t.Fatalf("expected public key to be:\n%#+v\nbut got:\n%#+v", testPublicKey, publicKeyParsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignAndVerify(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
payload []byte
|
||||||
|
}{
|
||||||
|
{[]byte("test my content 1")},
|
||||||
|
{[]byte("test my content 2")},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
sig, err := Sign(testPrivateKey, tt.payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("[%d] sign error: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := Verify(testPublicKey, sig, tt.payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("[%d] verify error: %v", i, err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("[%d] verification failed for '%s'", i, tt.payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test with other, invalid public key, should fail to verify.
|
||||||
|
tempPublicKey := &MustGenerateKey().PublicKey
|
||||||
|
|
||||||
|
ok, err = Verify(tempPublicKey, sig, tt.payload)
|
||||||
|
if ok {
|
||||||
|
t.Fatalf("[%d] verification should fail but passed for '%s'", i, tt.payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
doc.go
2
doc.go
|
@ -494,7 +494,7 @@ Example code:
|
||||||
// http://myhost.com/users/42/profile
|
// http://myhost.com/users/42/profile
|
||||||
users.Get("/{id:uint64}/profile", userProfileHandler)
|
users.Get("/{id:uint64}/profile", userProfileHandler)
|
||||||
// http://myhost.com/users/messages/1
|
// http://myhost.com/users/messages/1
|
||||||
users.Get("/inbox/{id:int}", userMessageHandler)
|
users.Get("/messages/{id:int}", userMessageHandler)
|
||||||
|
|
||||||
|
|
||||||
Custom HTTP Errors
|
Custom HTTP Errors
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -15,7 +15,7 @@ require (
|
||||||
github.com/iris-contrib/go.uuid v2.0.0+incompatible
|
github.com/iris-contrib/go.uuid v2.0.0+incompatible
|
||||||
github.com/json-iterator/go v1.1.6 // vendor removed.
|
github.com/json-iterator/go v1.1.6 // vendor removed.
|
||||||
github.com/kataras/golog v0.0.0-20180321173939-03be10146386
|
github.com/kataras/golog v0.0.0-20180321173939-03be10146386
|
||||||
github.com/kataras/neffos v0.0.1
|
github.com/kataras/neffos v0.0.2
|
||||||
github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d // indirect
|
github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d // indirect
|
||||||
github.com/microcosm-cc/bluemonday v1.0.2
|
github.com/microcosm-cc/bluemonday v1.0.2
|
||||||
github.com/ryanuber/columnize v2.1.0+incompatible
|
github.com/ryanuber/columnize v2.1.0+incompatible
|
||||||
|
|
Loading…
Reference in New Issue
Block a user