[series] - go in API routing middleware signature verification

Keywords: Go PHP Unix github

overview

First, synchronize the following project overview:

The last article shared that Jaeger link tracking, a routing middleware, had an unexpected response. The "Go China" public account was also forwarded. Many friends joined me to communicate with each other and called me "God". In fact, what God am I? It's just a local practice. I'm still a newcomer to the use of Go language. Thank you for your kindness. Love!

This article we share: routing middleware signature verification.

Why signature verification?

Needless to say, this is mainly to ensure the security of the interface and identify the caller. Based on these two points, let's design a signature together.

The caller needs to apply for App Key and App Secret. App Key is used to identify the caller and App Secret is used to encrypt and generate signature.

Of course, the generated signature also needs to meet the following requirements:

  • Variability: each signature must be different.
  • Timeliness: timeliness of each request, expired and invalid.
  • Uniqueness: each signature is unique.
  • Integrity: it can verify the incoming data and prevent tampering.

for instance:

/API? Param UU 1 = XXX & param UU 2 = XXX, where param UU 1 and param UU 2 are two parameters.

If signature verification is added, several more parameters need to be passed:

  • ak represents App Key, which is used to identify the caller.
  • ts represents the time stamp used to verify the timeliness of the interface.
  • sn represents the signature encryption string, which is used to verify the integrity of data and prevent data tampering.

sn is encrypted through App Secret and passed parameters.

The final parameters passed are as follows:

/api?param_1=xxx&param_2=xxx&ak=xxx&ts=xxx&sn=xxx

It's a debugging skill. It's too troublesome to generate ts and sn parameters manually every time. When debug=1 is passed, TS and sn will be returned. See the code for details.

This article shares three ways to implement signature: MD5 combined encryption, AES symmetric encryption and RSA asymmetric encryption.

Don't talk too much, get into the subject.

MD5 combination

Generate signature

First, encapsulate an MD5 method of Go:

func MD5(str string) string {
    s := md5.New()
    s.Write([]byte(str))
    return hex.EncodeToString(s.Sum(nil))
}

To encrypt:

appKey     = "demo"
appSecret  = "xxx"
encryptStr = "param_1=xxx&param_2=xxx&ak="+appKey+"&ts=xxx"

// Custom validation rules
sn = MD5(appSecret + encryptStr + appSecret)

Verifying signature

By passing the parameters, the signature is generated again. If the passed signature is compared with the generated signature.

Same, indicating that the signature verification is successful.

Different, indicating that signature verification failed.

Middleware code implementation

var AppSecret string

// MD5 combined encryption
func SetUp() gin.HandlerFunc {

    return func(c *gin.Context) {
        utilGin := util.Gin{Ctx: c}

        sign, err := verifySign(c)

        if sign != nil {
            utilGin.Response(-1, "Debug Sign", sign)
            c.Abort()
            return
        }

        if err != nil {
            utilGin.Response(-1, err.Error(), sign)
            c.Abort()
            return
        }

        c.Next()
    }
}

// Verifying signature
func verifySign(c *gin.Context) (map[string]string, error) {
    _ = c.Request.ParseForm()
    req   := c.Request.Form
    debug := strings.Join(c.Request.Form["debug"], "")
    ak    := strings.Join(c.Request.Form["ak"], "")
    sn    := strings.Join(c.Request.Form["sn"], "")
    ts    := strings.Join(c.Request.Form["ts"], "")

    // Source of validation
    value, ok := config.ApiAuthConfig[ak]
    if ok {
        AppSecret = value["md5"]
    } else {
        return nil, errors.New("ak Error")
    }

    if debug == "1" {
        currentUnix := util.GetCurrentUnix()
        req.Set("ts", strconv.FormatInt(currentUnix, 10))
        res := map[string]string{
            "ts": strconv.FormatInt(currentUnix, 10),
            "sn": createSign(req),
        }
        return res, nil
    }

    // Validation expiration time
    timestamp := time.Now().Unix()
    exp, _    := strconv.ParseInt(config.AppSignExpiry, 10, 64)
    tsInt, _  := strconv.ParseInt(ts, 10, 64)
    if tsInt > timestamp || timestamp - tsInt >= exp {
        return nil, errors.New("ts Error")
    }

    // Verifying signature
    if sn == "" || sn != createSign(req) {
        return nil, errors.New("sn Error")
    }

    return nil, nil
}

// Create signature
func createSign(params url.Values) string {
    // Custom MD5 combination
    return util.MD5(AppSecret + createEncryptStr(params) + AppSecret)
}

func createEncryptStr(params url.Values) string {
    var key []string
    var str = ""
    for k := range params {
        if k != "sn" && k != "debug" {
            key = append(key, k)
        }
    }
    sort.Strings(key)
    for i := 0; i < len(key); i++ {
        if i == 0 {
            str = fmt.Sprintf("%v=%v", key[i], params.Get(key[i]))
        } else {
            str = str + fmt.Sprintf("&%v=%v", key[i], params.Get(key[i]))
        }
    }
    return str
}

AES symmetric encryption

Before we use it, let's understand what symmetric encryption is.

Symmetric encryption is to use the same key that can be encrypted or decrypted. This method is called symmetric encryption.

Common algorithms: DES, AES.

AES is the upgraded version of DES, with longer key length, more options, more flexibility, higher security and faster speed. Let's directly start AES encryption.

Advantage

The algorithm is open, the computation is small, the encryption speed is fast, and the encryption efficiency is high.

shortcoming

The sender and the receiver must agree on the key, and then both parties can save the key, so the key management becomes the burden of both parties.

Application scenario

A relatively large amount of data or encryption of critical data.

Generate signature

First, encapsulate the AesEncrypt encryption method and AesDecrypt decryption method of Go.

// Encryption AES? 128? CBC
func AesEncrypt (encryptStr string, key []byte, iv string) (string, error) {
    encryptBytes := []byte(encryptStr)
    block, err   := aes.NewCipher(key)
    if err != nil {
        return "", err
    }

    blockSize := block.BlockSize()
    encryptBytes = pkcs5Padding(encryptBytes, blockSize)

    blockMode := cipher.NewCBCEncrypter(block, []byte(iv))
    encrypted := make([]byte, len(encryptBytes))
    blockMode.CryptBlocks(encrypted, encryptBytes)
    return base64.URLEncoding.EncodeToString(encrypted), nil
}

// Decrypt
func AesDecrypt (decryptStr string, key []byte, iv string) (string, error) {
    decryptBytes, err := base64.URLEncoding.DecodeString(decryptStr)
    if err != nil {
        return "", err
    }

    block, err := aes.NewCipher(key)
    if err != nil {
        return "", err
    }

    blockMode := cipher.NewCBCDecrypter(block, []byte(iv))
    decrypted := make([]byte, len(decryptBytes))

    blockMode.CryptBlocks(decrypted, decryptBytes)
    decrypted = pkcs5UnPadding(decrypted)
    return string(decrypted), nil
}

func pkcs5Padding (cipherText []byte, blockSize int) []byte {
    padding := blockSize - len(cipherText)%blockSize
    padText := bytes.Repeat([]byte{byte(padding)}, padding)
    return append(cipherText, padText...)
}

func pkcs5UnPadding (decrypted []byte) []byte {
    length := len(decrypted)
    unPadding := int(decrypted[length-1])
    return decrypted[:(length - unPadding)]
}

To encrypt:

appKey     = "demo"
appSecret  = "xxx"
encryptStr = "param_1=xxx&param_2=xxx&ak="+appKey+"&ts=xxx"

sn = AesEncrypt(encryptStr, appSecret)

Verifying signature

decryptStr = AesDecrypt(sn, app_secret)

Compare the encrypted string with the decrypted one.

Same, indicating that the signature verification is successful.

Different, indicating that signature verification failed.

Middleware code implementation

var AppSecret string

// AES symmetric encryption
func SetUp() gin.HandlerFunc {

    return func(c *gin.Context) {
        utilGin := util.Gin{Ctx: c}

        sign, err := verifySign(c)

        if sign != nil {
            utilGin.Response(-1, "Debug Sign", sign)
            c.Abort()
            return
        }

        if err != nil {
            utilGin.Response(-1, err.Error(), sign)
            c.Abort()
            return
        }

        c.Next()
    }
}

// Verifying signature
func verifySign(c *gin.Context) (map[string]string, error) {
    _ = c.Request.ParseForm()
    req   := c.Request.Form
    debug := strings.Join(c.Request.Form["debug"], "")
    ak    := strings.Join(c.Request.Form["ak"], "")
    sn    := strings.Join(c.Request.Form["sn"], "")
    ts    := strings.Join(c.Request.Form["ts"], "")

    // Source of validation
    value, ok := config.ApiAuthConfig[ak]
    if ok {
        AppSecret = value["aes"]
    } else {
        return nil, errors.New("ak Error")
    }

    if debug == "1" {
        currentUnix := util.GetCurrentUnix()
        req.Set("ts", strconv.FormatInt(currentUnix, 10))

        sn, err := createSign(req)
        if err != nil {
            return nil, errors.New("sn Exception")
        }

        res := map[string]string{
            "ts": strconv.FormatInt(currentUnix, 10),
            "sn": sn,
        }
        return res, nil
    }

    // Validation expiration time
    timestamp := time.Now().Unix()
    exp, _    := strconv.ParseInt(config.AppSignExpiry, 10, 64)
    tsInt, _  := strconv.ParseInt(ts, 10, 64)
    if tsInt > timestamp || timestamp - tsInt >= exp {
        return nil, errors.New("ts Error")
    }

    // Verifying signature
    if sn == "" {
        return nil, errors.New("sn Error")
    }

    decryptStr, decryptErr := util.AesDecrypt(sn, []byte(AppSecret), AppSecret)
    if decryptErr != nil {
        return nil, errors.New(decryptErr.Error())
    }
    if decryptStr != createEncryptStr(req) {
        return nil, errors.New("sn Error")
    }
    return nil, nil
}

// Create signature
func createSign(params url.Values) (string, error) {
    return util.AesEncrypt(createEncryptStr(params), []byte(AppSecret), AppSecret)
}

func createEncryptStr(params url.Values) string {
    var key []string
    var str = ""
    for k := range params {
        if k != "sn" && k != "debug" {
            key = append(key, k)
        }
    }
    sort.Strings(key)
    for i := 0; i < len(key); i++ {
        if i == 0 {
            str = fmt.Sprintf("%v=%v", key[i], params.Get(key[i]))
        } else {
            str = str + fmt.Sprintf("&%v=%v", key[i], params.Get(key[i]))
        }
    }
    return str
}

RSA asymmetric encryption

As above, before using, let's understand what is asymmetric encryption?

Asymmetric encryption requires two keys for encryption and decryption. The two keys are public key and private key. This method is called asymmetric encryption.

Common algorithm: RSA.

Advantage

Compared with symmetric encryption, it has better security. Different keys are needed for encryption and decryption. Both public key and private key can encrypt and decrypt each other.

shortcoming

Encryption and decryption take a long time, slow speed, only suitable for a small amount of data encryption.

Application scenario

It is suitable for scenarios with high security requirements, and for encrypting a small amount of data, such as payment data, login data, etc.

Create signature

First, we encapsulate the RSA public key encryption method and the RSA private decrypt decryption method of Go.

// Public key encryption
func RsaPublicEncrypt(encryptStr string, path string) (string, error) {
    // Open file
    file, err := os.Open(path)
    if err != nil {
        return "", err
    }
    defer file.Close()

    // Read file contents
    info, _ := file.Stat()
    buf := make([]byte,info.Size())
    file.Read(buf)

    // pem decoding
    block, _ := pem.Decode(buf)

    // x509 decoding
    publicKeyInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
    if err != nil {
        return "", err
    }

    // Type Asserts
    publicKey := publicKeyInterface.(*rsa.PublicKey)

    //Encrypt plaintext
    encryptedStr, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, []byte(encryptStr))
    if err != nil {
        return "", err
    }

    //Return ciphertext
    return base64.URLEncoding.EncodeToString(encryptedStr), nil
}

// Private key decryption
func RsaPrivateDecrypt(decryptStr string, path string) (string, error) {
    // Open file
    file, err := os.Open(path)
    if err != nil {
        return "", err
    }
    defer file.Close()

    // Get file content
    info, _ := file.Stat()
    buf := make([]byte,info.Size())
    file.Read(buf)

    // pem decoding
    block, _ := pem.Decode(buf)

    // X509 decoding
    privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
    if err != nil {
        return "", err
    }
    decryptBytes, err := base64.URLEncoding.DecodeString(decryptStr)

    //Decrypt the ciphertext
    decrypted, _ := rsa.DecryptPKCS1v15(rand.Reader,privateKey,decryptBytes)

    //Return plaintext
    return string(decrypted), nil
}

The caller applies for a public key and encrypts it:

appKey     = "demo"
appSecret  = "Public key"
encryptStr = "param_1=xxx&param_2=xxx&ak="+appKey+"&ts=xxx"

sn = RsaPublicEncrypt(encryptStr, appSecret)

Verifying signature

decryptStr = RsaPrivateDecrypt(sn, app_secret)

Compare the encrypted string with the decrypted one.

Same, indicating that the signature verification is successful.

Different, indicating that signature verification failed.

Middleware code implementation

var AppSecret string

// RSA asymmetric encryption
func SetUp() gin.HandlerFunc {

    return func(c *gin.Context) {
        utilGin := util.Gin{Ctx: c}

        sign, err := verifySign(c)

        if sign != nil {
            utilGin.Response(-1, "Debug Sign", sign)
            c.Abort()
            return
        }

        if err != nil {
            utilGin.Response(-1, err.Error(), sign)
            c.Abort()
            return
        }

        c.Next()
    }
}

// Verifying signature
func verifySign(c *gin.Context) (map[string]string, error) {
    _ = c.Request.ParseForm()
    req   := c.Request.Form
    debug := strings.Join(c.Request.Form["debug"], "")
    ak    := strings.Join(c.Request.Form["ak"], "")
    sn    := strings.Join(c.Request.Form["sn"], "")
    ts    := strings.Join(c.Request.Form["ts"], "")

    // Source of validation
    value, ok := config.ApiAuthConfig[ak]
    if ok {
        AppSecret = value["rsa"]
    } else {
        return nil, errors.New("ak Error")
    }

    if debug == "1" {
        currentUnix := util.GetCurrentUnix()
        req.Set("ts", strconv.FormatInt(currentUnix, 10))

        sn, err := createSign(req)
        if err != nil {
            return nil, errors.New("sn Exception")
        }

        res := map[string]string{
            "ts": strconv.FormatInt(currentUnix, 10),
            "sn": sn,
        }
        return res, nil
    }

    // Validation expiration time
    timestamp := time.Now().Unix()
    exp, _    := strconv.ParseInt(config.AppSignExpiry, 10, 64)
    tsInt, _  := strconv.ParseInt(ts, 10, 64)
    if tsInt > timestamp || timestamp - tsInt >= exp {
        return nil, errors.New("ts Error")
    }

    // Verifying signature
    if sn == "" {
        return nil, errors.New("sn Error")
    }

    decryptStr, decryptErr := util.RsaPrivateDecrypt(sn, config.AppRsaPrivateFile)
    if decryptErr != nil {
        return nil, errors.New(decryptErr.Error())
    }
    if decryptStr != createEncryptStr(req) {
        return nil, errors.New("sn Error")
    }
    return nil, nil
}

// Create signature
func createSign(params url.Values) (string, error) {
    return util.RsaPublicEncrypt(createEncryptStr(params), AppSecret)
}

func createEncryptStr(params url.Values) string {
    var key []string
    var str = ""
    for k := range params {
        if k != "sn" && k != "debug" {
            key = append(key, k)
        }
    }
    sort.Strings(key)
    for i := 0; i < len(key); i++ {
        if i == 0 {
            str = fmt.Sprintf("%v=%v", key[i], params.Get(key[i]))
        } else {
            str = str + fmt.Sprintf("&%v=%v", key[i], params.Get(key[i]))
        }
    }
    return str
}

How to call?

Like other middleware invocation methods, it can be freely selected according to its own needs.

For example, use MD5 combination:

.Use(sign_md5.SetUp())

Use AES symmetric encryption:

.Use(sign_aes.SetUp())

Use RSA asymmetric encryption:

.Use(sign_rsa.SetUp())

performance testing

Since RSA asymmetric encryption is the most secure, use it uniformly.

NO! NO! NO! Absolutely not!

Why should I be excited? Because I've met this pit before. It's all a tearful lesson...

Let's test the performance one by one:

MD5

func Md5Test(c *gin.Context) {
    startTime  := time.Now()
    appSecret  := "IgkibX71IEf382PT"
    encryptStr := "param_1=xxx&param_2=xxx&ak=xxx&ts=1111111111"
    count      := 1000000
    for i := 0; i < count; i++ {
        // Generate signature
        util.MD5(appSecret + encryptStr + appSecret)

        // Verifying signature
        util.MD5(appSecret + encryptStr + appSecret)
    }
    utilGin := util.Gin{Ctx: c}
    utilGin.Response(1, fmt.Sprintf("%v second - %v", count, time.Since(startTime)), nil)
}

Simulate one million requests, and the execution time is about 1.1s ~ 1.2s.

AES

func AesTest(c *gin.Context) {
    startTime  := time.Now()
    appSecret  := "IgkibX71IEf382PT"
    encryptStr := "param_1=xxx&param_2=xxx&ak=xxx&ts=1111111111"
    count      := 1000000
    for i := 0; i < count; i++ {
        // Generate signature
        sn, _ := util.AesEncrypt(encryptStr, []byte(appSecret), appSecret)

        // Verifying signature
        util.AesDecrypt(sn, []byte(appSecret), appSecret)
    }
    utilGin := util.Gin{Ctx: c}
    utilGin.Response(1, fmt.Sprintf("%v second - %v", count, time.Since(startTime)), nil)
}

Simulate one million requests, and the execution time is about 1.8s ~ 1.9s.

RSA

func RsaTest(c *gin.Context) {
    startTime  := time.Now()
    encryptStr := "param_1=xxx&param_2=xxx&ak=xxx&ts=1111111111"
    count      := 500
    for i := 0; i < count; i++ {
        // Generate signature
        sn, _ := util.RsaPublicEncrypt(encryptStr, "rsa/public.pem")

        // Verifying signature
        util.RsaPrivateDecrypt(sn, "rsa/private.pem")
    }
    utilGin := util.Gin{Ctx: c}
    utilGin.Response(1, fmt.Sprintf("%v second - %v", count, time.Since(startTime)), nil)
}

I dare not simulate a million requests. I don't know when I can handle it. Let's simulate 500 times.

Simulate 500 requests, and the execution time is about 1s.

The above is my local execution effect. You can question my computer's poor performance and the packaging method.

You can also try to see if the performance gap is so large.

How do PHP and Go encryption methods communicate?

I write PHP. Can I use PHP to generate signatures?

Definitely!

I also implemented the above three methods in PHP. There may be some small adjustments. The overall problem is not big. The relevant Demo has been uploaded to github:

https://github.com/xinliangno...

OK, that's it.

Source address

https://github.com/xinliangno...

Go in API series

Posted by hustler on Sat, 26 Oct 2019 03:13:21 -0700