mirror of
https://github.com/kataras/iris.git
synced 2025-03-14 02:56:26 +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)
|
||||
- [JWT](experimental-handlers/jwt/main.go)
|
||||
- [Sessions](#sessions)
|
||||
- [Request Authentication](authentication/request/main.go) **NEW**
|
||||
|
||||
### 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
|
||||
users.Get("/{id:uint64}/profile", userProfileHandler)
|
||||
// http://myhost.com/users/messages/1
|
||||
users.Get("/inbox/{id:int}", userMessageHandler)
|
||||
users.Get("/messages/{id:int}", userMessageHandler)
|
||||
|
||||
|
||||
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/json-iterator/go v1.1.6 // vendor removed.
|
||||
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/microcosm-cc/bluemonday v1.0.2
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible
|
||||
|
|
Loading…
Reference in New Issue
Block a user