diff --git a/_examples/README.md b/_examples/README.md index a7a0a6709..f9b1c3c57 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -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 diff --git a/_examples/authentication/request/main.go b/_examples/authentication/request/main.go new file mode 100644 index 000000000..1d3ff8f4b --- /dev/null +++ b/_examples/authentication/request/main.go @@ -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. diff --git a/crypto/crypto.go b/crypto/crypto.go new file mode 100644 index 000000000..ca7889e1d --- /dev/null +++ b/crypto/crypto.go @@ -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 +} diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go new file mode 100644 index 000000000..631d5f553 --- /dev/null +++ b/crypto/crypto_test.go @@ -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[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") +} diff --git a/crypto/gcm/gcm.go b/crypto/gcm/gcm.go new file mode 100644 index 000000000..17c9197ec --- /dev/null +++ b/crypto/gcm/gcm.go @@ -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 +} diff --git a/crypto/gcm/gcm_test.go b/crypto/gcm/gcm_test.go new file mode 100644 index 000000000..737af226d --- /dev/null +++ b/crypto/gcm/gcm_test.go @@ -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) + } + } +} diff --git a/crypto/json.go b/crypto/json.go new file mode 100644 index 000000000..09f97f91c --- /dev/null +++ b/crypto/json.go @@ -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 +} diff --git a/crypto/json_test.go b/crypto/json_test.go new file mode 100644 index 000000000..1f8bea9d6 --- /dev/null +++ b/crypto/json_test.go @@ -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) + } +} diff --git a/crypto/sign/sign.go b/crypto/sign/sign.go new file mode 100644 index 000000000..9c4a1d3c9 --- /dev/null +++ b/crypto/sign/sign.go @@ -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 +} diff --git a/crypto/sign/sign_test.go b/crypto/sign/sign_test.go new file mode 100644 index 000000000..0cb4653ea --- /dev/null +++ b/crypto/sign/sign_test.go @@ -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) + } + } +} diff --git a/doc.go b/doc.go index 2ecf9a7d5..a97109ad9 100644 --- a/doc.go +++ b/doc.go @@ -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 diff --git a/go.mod b/go.mod index 0b1a2a366..9afb9364e 100644 --- a/go.mod +++ b/go.mod @@ -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