Skip to content

Commit

Permalink
implement the Iris Crypto Library for Request Authentication and Veri…
Browse files Browse the repository at this point in the history
…fication. With Examples and Tests.

Relative to this one as well: kataras#1200


Former-commit-id: 3a29e7398b7fdeb9b48a118b742d419d5681d56b
  • Loading branch information
kataras committed Jul 2, 2019
1 parent 35389c6 commit 9dbb300
Show file tree
Hide file tree
Showing 12 changed files with 992 additions and 2 deletions.
1 change: 1 addition & 0 deletions _examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
137 changes: 137 additions & 0 deletions _examples/authentication/request/main.go
Original file line number Diff line number Diff line change
@@ -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 changes: 149 additions & 0 deletions crypto/crypto.go
Original file line number Diff line number Diff line change
@@ -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 changes: 96 additions & 0 deletions crypto/crypto_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
Loading

0 comments on commit 9dbb300

Please sign in to comment.