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:
Gerasimos (Makis) Maropoulos 2019-07-02 19:19:43 +03:00
parent 35389c6ef8
commit 9dbb300d9b
12 changed files with 992 additions and 2 deletions

View File

@ -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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -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
View File

@ -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