Remove the two day old crypto package, let's not confuse users between that and JWT - keep only JWT. And also, Drop older go versions support, only go1.12 and above which comes with a lot of performance improvements and security patches

Former-commit-id: a1d791ce45b87ecc440ed9c5aee5d5d204763f0d
This commit is contained in:
Gerasimos (Makis) Maropoulos 2019-07-09 12:42:27 +03:00
parent 2567ed22be
commit 29deac3270
15 changed files with 5 additions and 1032 deletions

View File

@ -3,9 +3,6 @@ os:
- linux - linux
- osx - osx
go: go:
- 1.9.x
- 1.10.x
- 1.11.x
- 1.12.x - 1.12.x
go_import_path: github.com/kataras/iris go_import_path: github.com/kataras/iris
# we disable test caching via GOCACHE=off # we disable test caching via GOCACHE=off

32
FAQ.md
View File

@ -24,6 +24,8 @@ Add a `badge` to your open-source projects powered by [Iris](https://iris-go.com
go get -u github.com/kataras/iris go get -u github.com/kataras/iris
``` ```
Go installed version 1.12 and above is required.
## Learning ## Learning
More than 100 practical examples, tutorials and articles at: More than 100 practical examples, tutorials and articles at:
@ -39,36 +41,6 @@ More than 100 practical examples, tutorials and articles at:
Want to help and join to the greatest community? Describe your skills and push your own sections at: https://github.com/kataras/build-a-better-web-together/issues/new Want to help and join to the greatest community? Describe your skills and push your own sections at: https://github.com/kataras/build-a-better-web-together/issues/new
### common errors that new gophers may meet
#### type aliases
| build error | reason | solution |
| -----------|--------|--------|
| `undefined iris.Context` | caused of using the **optional type alias** `iris.Context` instead of the `context.Context` when building with Go 1.8 | import the original package `github.com/kataras/iris/context` and declare as `func(context.Context){})` **or** download and install the [latest go version](https://golang.org/dl) _recommended_ |
Type alias is a new feature, introduced at Go version 1.9, so if you want to use Iris' type aliases you have to build using the latest Go version. Nothing really changes for your application if you use type alias or not, Iris' type aliases helps you to omit import statements -- to reduce lines of code, nothing more.
**Details...**
Go version 1.9 introduced the [type alias](https://golang.org/doc/go1.9#language) feature.
Iris uses the `type alias` feature to help you writing less code by omitting some package imports. The examples and documentation are written using Go 1.9 as well.
If you build your Go app with Go 1.9 you can, optionally, use all Iris web framework's features by importing one single package, the `github.com/kataras/iris`.
Available type aliases;
| Go 1.8 | Go 1.8 usage | Go 1.9 usage (optionally) |
| -----------|--------|--------|
| `import "github.com/kataras/iris/context"` | `func(context.Context) {}`, `context.Handler`, `context.Map` | `func(iris.Context) {}`, `iris.Handler`, `iris.Map` |
| `import "github.com/kataras/iris/core/router"` | `app.PartyFunc("/users", func(p router.Party) {})`, `router.ExecutionOptions`, `router.ExecutionRules` | `app.PartyFunc("/users", func(p iris.Party) {})`, `iris.ExecutionOptions`, `iris.ExecutionRules` |
| `import "github.com/kataras/iris/core/host"` | `app.ConfigureHost(func(s *host.Supervisor) {})` | `app.ConfigureHost(func(s *iris.Supervisor) {})` |
You can find all type aliases and their original package import statements at the [./context.go file](context.go).
> Remember; this doesn't mean that you have to use those type aliases, you can still import the original packages as you did with Go version 1.8, it's up to you.
## Active development mode ## Active development mode
Iris may have reached version 11, but we're not stopping there. We have many feature ideas on our board that we're anxious to add and other innovative web development solutions that we're planning to build into Iris. Iris may have reached version 11, but we're not stopping there. We have many feature ideas on our board that we're anxious to add and other innovative web development solutions that we're planning to build into Iris.

View File

@ -44,13 +44,13 @@ Iris does not force you to use any specific ORM or template engine. With support
## Installation ## Installation
The only requirement is the [Go Programming Language](https://golang.org/dl/) The only requirement is the [Go Programming Language](https://golang.org/dl/).
```sh ```sh
$ go get -u github.com/kataras/iris $ go get -u github.com/kataras/iris
``` ```
Iris takes advantage of the [vendor directory](https://docs.google.com/document/d/1Bz5-UB7g2uPBdOx-rw5t9MxJwkfpx90cqG9AFL0JAYo) feature. You get truly reproducible builds, as this method guards against upstream renames and deletes. Iris takes advantage of the [vendor directory](https://docs.google.com/document/d/1Bz5-UB7g2uPBdOx-rw5t9MxJwkfpx90cqG9AFL0JAYo) feature. You get truly reproducible builds, as this method guards against upstream renames and deletes. The minimum required Go version to use Iris is go1.12.
<details> <details>
<summary>Known issues for code editors and IDEs at general</summary> <summary>Known issues for code editors and IDEs at general</summary>

View File

@ -363,7 +363,6 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her
- [Basic Authentication](authentication/basicauth/main.go) - [Basic Authentication](authentication/basicauth/main.go)
- [OAUth2](authentication/oauth2/main.go) - [OAUth2](authentication/oauth2/main.go)
- [Request Auth](request/main.go) **NEW**
- [Request Auth(JWT)](experimental-handlers/jwt/main.go) - [Request Auth(JWT)](experimental-handlers/jwt/main.go)
- [Sessions](#sessions) - [Sessions](#sessions)

View File

@ -2,6 +2,5 @@
- [Basic Authentication](basicauth/main.go) - [Basic Authentication](basicauth/main.go)
- [OAUth2](oauth2/main.go) - [OAUth2](oauth2/main.go)
- [Request Auth](request/main.go)
- [Request Auth(JWT)](https://github.com/kataras/iris/blob/master/_examples/experimental-handlers/jwt/main.go) - [Request Auth(JWT)](https://github.com/kataras/iris/blob/master/_examples/experimental-handlers/jwt/main.go)
- [Sessions](https://github.com/kataras/iris/tree/master/_examples/#sessions) - [Sessions](https://github.com/kataras/iris/tree/master/_examples/#sessions)

View File

@ -1,138 +0,0 @@
package main
import (
"io/ioutil"
"github.com/kataras/iris"
"github.com/kataras/iris/crypto"
)
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
)
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.
//
// See experimental-handlers/jwt example to use JWT for request authentication instead.

View File

@ -1,153 +0,0 @@
package crypto
import (
"crypto/ecdsa"
"encoding/base64"
"github.com/kataras/iris/crypto/gcm"
"github.com/kataras/iris/crypto/sign"
)
var (
// MustGenerateKey generates an ecdsa private and public key pair.
// It panics if any error occurred.
MustGenerateKey = sign.MustGenerateKey
// ParsePrivateKey accepts a pem x509-encoded private key and decodes to *ecdsa.PrivateKey.
ParsePrivateKey = sign.ParsePrivateKey
// ParsePublicKey accepts a pem x509-encoded public key and decodes to *ecdsa.PrivateKey.
ParsePublicKey = sign.ParsePublicKey
// 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(ctx.Request().Body)
// signedData, err := crypto.Marshal(testPrivateKey, data, nil)
// ctx.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
}

View File

@ -1,96 +0,0 @@
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")
}

View File

@ -1,134 +0,0 @@
// 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
}

View File

@ -1,46 +0,0 @@
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)
}
}
}

View File

@ -1,94 +0,0 @@
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 `SignJSON`).
//
// Returns true whether 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
}

View File

@ -1,114 +0,0 @@
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)
}
}

View File

@ -1,145 +0,0 @@
// 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 private and public 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 private and public 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
}

View File

@ -1,74 +0,0 @@
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
View File

@ -42,7 +42,7 @@ Current Version
Installation Installation
The only requirement is the Go Programming Language, at least version 1.8 but 1.11.1 and above is highly recommended. The only requirement is the Go Programming Language, at least version 1.12.
$ go get -u github.com/kataras/iris $ go get -u github.com/kataras/iris