forked from kataras/iris
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
implement the Iris Crypto Library for Request Authentication and Veri…
…fication. With Examples and Tests. Relative to this one as well: kataras#1200 Former-commit-id: 3a29e7398b7fdeb9b48a118b742d419d5681d56b
- Loading branch information
Showing
12 changed files
with
992 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
Oops, something went wrong.