replace the custom crypto with the jwt one, it's working fine now

Gerasimos (Makis) Maropoulos 2019-07-20 07:33:06 +03:00
parent e63227d325
commit 3ef4d7bdfd
No known key found for this signature in database
GPG Key ID: F169457BBDA4ACF4

@ -1,389 +1,72 @@
Iris offers secure and modern request authentication for your web applications. Signing and verifying data featured inside the iris/crypto subpackage. Like the cryptocurrencies, Iris uses the Elliptic Curve Digital Signature Algorithm to sign and verify data.
Iris offers request authentication through its [jwt middleware](https://github.com/iris-contrib/middleware/tree/master/jwt). In this chapter you will learn the basics of how to use JWT with Iris.
The Iris crypto package offers authentication (with optional encryption in top of) and verification of raw `[]byte` data with `crypto.Marshal/Unmarshal` package-level functions.
**1.** Install it by executing the following shell command:
Higher level implementation is also available. The `crypto.SignJSON/VerifyJSON` package-level functions accept JSON payloads.
> You can skip the below section and take ahead to the example code if you already know what you're doing.
## ECDSA Private - Public Key pair
Let's begin by learning how you can generate or parse an existing ECDSA private and public key pair which is required for signing and verification. The private key is used for signing, the public key is used for verification.
### Generate
The `MustGenerateKey` package-level function generates an ECDSA private and public key pair. It panics if any error occurred.
```go
MustGenerateKey() *ecdsa.PrivateKey
```sh
$ go get github.com/iris-contrib/middleware/jwt
```
### Parse
**2.** To create a new jwt middleware use the `jwt.New` function. This example extracts the token through a `"token"` url parameter. Authenticated clients should be designed to set that with a signed token. The default jwt middleware's behavior to extract a token value is by the `Authentication: Bearer $TOKEN` header.
However, on a production level application you may have already stored a private key somewhere in the local system or to a database per client.
The jwt middleware has three methods to validate tokens.
There are two functions, when you have access to the private key use the `ParsePrivateKey`. Remember, the [*ecdsa.PrivateKey](https://golang.org/pkg/crypto/ecdsa/#PrivateKey) can provide you its public key through its `.PublicKey` field. Otherwise if you just want the public key use the `ParsePublicKey` instead.
- The first one is the `Serve` method - it is an `iris.Handler`,
- the second one is the `CheckJWT(iris.Context) bool` and
- the third one is a helper to retrieve the validated token - the `Get(iris.Context) *jwt.Token`.
**3.** To register it you simply prepend the jwt `j.Serve` middleware to a specific group of routes, a single route, or globally e.g. `app.Get("/secured", j.Serve, myAuthenticatedHandler)`.
**4.** To generate a token inside a handler that accepts a user payload and responses the signed token which later on can be sent through client requests headers or url parameter in this case, e.g. `jwt.NewToken` or `jwt.NewTokenWithClaims`.
## Example
```go
ParsePrivateKey(key []byte) (*ecdsa.PrivateKey, error)
ParsePublicKey(key []byte) (*ecdsa.PublicKey, error)
```
Both of these functions accept any `key []byte`, it may be retrieved from a database or from a local file reader, the implementation doesn't really care from where you access this.
## Sign and Verify JSON
Let's continue by taking look at the `SignJSON` and `VerifyJSON` as this is the most common scenario for a web application.
The `SignJSON` package-level function signs the incoming JSON request payload based on
a "privateKey" and the "r" (could be ctx.Request().Body).
It generates the signature and returns a `Ticket` structured value.
The `Ticket` output value contains the original client's payload raw data
and the generated signature.
```go
SignJSON(privateKey *ecdsa.PrivateKey, r io.Reader) (Ticket, error)
```
**Usage**
```go
ticket, err := crypto.SignJSON(testPrivateKey, ctx.Request().Body)
// ticket.Signature is the ECDSA signature.
// ticket.Payload is the original payload readen from the request body.
```
The `VerifyJSON` package-level function 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 `SignJSON`).
Returns true whether the verification succeed or not.
The "ptr" input argument 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 output arguments) and
2. ticket's original json payload parsed and "ptr" is filled successfully (error output argument).
```go
VerifyJSON(publicKey *ecdsa.PublicKey, r io.Reader, ptr interface{}) (bool, error)
```
**Usage**
```go
var myPayload myJSONStruct
ok, err := crypto.VerifyJSON(publicKey, ctx.Request().Body, &myPayload)
```
## Sign and Verify a slice of `[]byte`
The lower-level implementation lives inside the `crypto.Marshal` and `crypto.Unmarshal` package-level functions. Both of them accept a `crypto.Encryption` or `crypto.Decryption` to encode and decode more securely but they are totally optional.
The `Marshal` function 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.
```go
Marshal(privateKey *ecdsa.PrivateKey, data []byte, enc Encryption) ([]byte, error)
```
**Usage**
```go
data, _ := ioutil.ReadAll(ctx.Request().Body)
signedData, err := crypto.Marshal(testPrivateKey, data, nil)
w.Write(signedData)
```
Or if data should be **encrypted**:
```go
signedEncryptedData, err := crypto.Marshal(
testPrivateKey,
data,
crypto.Encrypt(aesKey, nil),
)
```
The `Unmarshal` function verifies the "data" and, optionally, decrypts the output.
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.
```go
Unmarshal(publicKey *ecdsa.PublicKey, data []byte, dec Decryption) ([]byte, bool)
```
**Usage**
```go
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**:
```go
verifiedDecryptedPayload, err := crypto.Unmarshal(
ecdsaPublicKey,
data,
crypto.Decrypt(aesKey, nil),
)
```
## About encryption
The `Encrypt` package-level function returns an `Encryption` type value 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.
```go
Encrypt(aesKey, additionalData []byte) Encryption
```
The `Decrypt` package-level function returns an `Decryption` type value 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.
```go
Decrypt(aesKey, additionalData []byte) Decryption
```
> Note that the `crypto.DefaultADATA` variable contains the default associated data used for `crypto.Encrypt` and `crypto.Decrypt` when "additionalData" is empty.
However, you can still use any custom encryption, the `crypto.Marshal and Unmarshal` functions accepts a `crypto.Encryption` and `crypto.Decryption` respectfully.
```go
// 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)
```
### The crypto/gcm subpackage
The underline encryption implementation is provided by the `iris/crypto/gcm` package.
We make use of the AES algorithm and the Galois/Counter Mode for our encryption/decription methods. Of course you can use the **`iris/crypto/gcm`** subpackage as standalone to other use cases inside your application as well.
The `gcm.MustGenerateKey` returns an AES key.
```go
MustGenerateKey() []byte
```
The `Encrypt` function of `gcm` subpackage encrypts and authenticates the plain data with any additional data and returns the ciphertext of it.
It uses the [AEAD](https://en.wikipedia.org/wiki/Authenticated_encryption) cipher mode providing authenticated encryption with associated
data.
The same additional data must be kept the same for `Decrypt`.
```go
Encrypt(aesKey, data, additionalData []byte) ([]byte, error)
```
The `Decrypt` function of the `gcm` subpackage 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`.
```go
Decrypt(aesKey, ciphertext, additionalData []byte) ([]byte, error)
```
References:
- 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
- https://en.wikipedia.org/wiki/Authenticated_encryption
--------
Finally it's time for some code.
Example Code:
Please read the _comments_ too for a better understanding.
```go
package main
import (
"io/ioutil"
"github.com/kataras/iris"
"github.com/kataras/iris/crypto"
"github.com/iris-contrib/middleware/jwt"
)
var (
// Change that to your own key.
// Usually 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
// crypto.ParsePrivateKey to convert data or local file to an *ecdsa.PrivateKey.
// and `crypto.ParsePublicKey` if you only have access to the public one.
testPrivateKey = crypto.MustGenerateKey()
testPublicKey = &testPrivateKey.PublicKey
)
func getTokenHandler(ctx iris.Context) {
token := jwt.NewTokenWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"foo": "bar",
})
type testPayloadStructure struct {
Key string `json:"key"`
Value string `json:"value"`
// Sign and get the complete encoded token as a string using the secret
tokenString, _ := token.SignedString([]byte("My Secret"))
ctx.HTML(`Token: ` + tokenString + `<br/><br/>
<a href="/secured?token=` + tokenString + `">/secured?token=` + tokenString + `</a>`)
}
// 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() {
func myAuthenticatedHandler(ctx iris.Context) {
user := ctx.Values().Get("jwt").(*jwt.Token)
ctx.Writef("This is an authenticated request\n")
ctx.Writef("Claim content:\n")
foobar := user.Claims.(jwt.MapClaims)
for key, value := range foobar {
ctx.Writef("%s = %s", key, value)
}
}
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
}
j := jwt.New(jwt.Config{
// Extract by "token" url parameter.
Extractor: jwt.FromParameter("token"),
// Send just the signature back
// ctx.WriteString(ticket.Signature)
// or the whole payload + the signature:
ctx.JSON(ticket)
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
return []byte("My Secret"), nil
},
SigningMethod: jwt.SigningMethodHS256,
})
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 for the shake of the example
// 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`.
// -
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%2FyxX
// iGN1k4Y%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.Get("/", getTokenHandler)
app.Get("/secured", j.Serve, myAuthenticatedHandler)
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.
You can use any payload as "claims".