內部API的安全防禦怎麼搞?密碼學中有答案

前言

事情的原由是公司以前的CDN服務是經過騰訊雲的COSFS來作的,它的好處是能夠像使用本地文件系統同樣直接操做騰訊雲對象存儲中的對象,但後來由於性能等因素,我花時間把上傳文件到CDN的功能用SDK重寫了(其實可能比搭個COSFS還簡單呢)。前端

前端同事剛好也有圖牀的使用需求,就想讓我給他們開個API,這樣他們就能夠直接經過代碼上傳文件了,而不用每次都找後端同事幫忙。這件事自己沒什麼難度,惟一的問題是這個API的安全方面如何保證,至少不能讓外人勿用。node

分析

其它業務上的API都是用的用戶登陸後的token及用戶的權限進行驗證,眼下這種用於開發需求的API雖然也能夠用一樣的方式來作,但一方面不夠方便(上傳個圖還要先登陸,想一想就麻煩),另外一方面安全性也仍是差些(是否是全部登陸的用戶都能調用呢?若是要再加權限的限制,也是麻煩)。git

剛好本身上份工做是作區塊鏈相關的,有一些密碼學的基礎知識,因此很天然想到用簽名驗籤的方式來作安全驗證。github

先簡單的解釋一下密碼學基礎知識,經常使用的加密方式有兩大類,一種是對稱加密,即加密和解密都是相同的祕鑰; 另外一種是非對稱加密,祕鑰有公私鑰之分,公鑰是用私鑰生成的。簽名是指要私鑰對一段信息的Hash加密,驗籤是指用私鑰對應的公鑰來驗證一段信息的簽名是否和信息匹配。非對稱加密原理的保證了簽名只能來自於私鑰,而只有對應在公鑰才能解籤。(若是你對這些原理感興趣,能夠自行搜索相關文章哈)golang

實現

咱們的後端是用的golang,前端是用NodeJS來實現的。非對稱加密的實現方式有好幾種,考慮到多端調試的成本,同時不想引入過多的第三方包,我這裏選擇了更加經常使用的RSA。下面將分爲後端和前端兩個部分來分別說明。json

後端實現

  1. 生成密鑰。方法有不少,能夠ssh的工具來生成,這是是用代碼來生成.
func GenerateKey(bits int) (*rsa.PrivateKey, *rsa.PublicKey, error) {
	private, err := rsa.GenerateKey(rand.Reader, bits)
	if err != nil {
		return nil, nil, err
	}
	return private, &private.PublicKey, nil
}
複製代碼

而後把密鑰導出成base64的字符串,方便保存和使用。後端

func EncodePrivateKey(private *rsa.PrivateKey) []byte {
	return pem.EncodeToMemory(&pem.Block{
		Bytes: x509.MarshalPKCS1PrivateKey(private),
		Type:  "RSA PRIVATE KEY",
	})
}

func EncodePublicKey(public *rsa.PublicKey) ([]byte, error) {
	publicBytes, err := x509.MarshalPKIXPublicKey(public)
	if err != nil {
		return nil, err
	}
	return pem.EncodeToMemory(&pem.Block{
		Bytes: publicBytes,
		Type:  "PUBLIC KEY",
	}), nil
}
複製代碼

代碼的話,沒什麼花頭,惟一須要注意的是Type不要亂填,這但是標準哈! pem也是最經常使用的的密鑰的編碼方式。安全

  1. 簽名解籤。
func SignWithSha256Base64(data string, prvKeyBytes []byte) (string, error) {
	block, _ := pem.Decode(prvKeyBytes)
	if block == nil {
		return "", errors.New("fail to decode private key")
	}

	privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		return "", err
	}
	h := sha256.New()
	h.Write([]byte([]byte(data)))
	hash := h.Sum(nil)
	signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hash[:])
	if err != nil {
		return "", err
	}
	out := base64.StdEncoding.EncodeToString(signature)
	return out, nil
}

func VerySignWithSha256Base64(originalData, signData string, pubKeyBytes[]byte) (bool, error) {
	sign, err := base64.StdEncoding.DecodeString(signData)
	if err != nil {
		return false ,err
	}
	block, _ := pem.Decode(pubKeyBytes)
	if block == nil {
		return false, errors.New("fail to decode public key")
	}

	pub, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return false, err
	}
	hash := sha256.New()
	hash.Write([]byte(originalData))
	err = rsa.VerifyPKCS1v15(pub.(*rsa.PublicKey), crypto.SHA256, hash.Sum(nil), sign)
	return err == nil, err
}
複製代碼

這裏選用的Hash方法是Sha256,在簽名和解籤時都要用Sha256哦。bash

其實,加解密的相關代碼在使用起來是比較固定的,可是必定是要記得使用的Hash方式和加解密的方法,要否則--嘿嘿--那真是調試到欲哭無淚呀。ssh

  1. 通過一層封裝,使用起來就很方便啦,下面測試一下。
func TestSignAndVerify(t *testing.T)  {
	sk, pk, _ := GenerateKey(1024)
	skBytes := EncodePrivateKey(sk)
	pkBytes, _ := EncodePublicKey(pk)
	fmt.Println(string(skBytes))
	fmt.Println(string(pkBytes))

	sig, err := SignWithSha256Base64("test", skBytes)
	if err != nil{
		fmt.Printf("%+v", err)
	}
	fmt.Println(sig)

	success, err := VerySignWithSha256Base64("test", sig, pkBytes)
	if success {
		fmt.Println("pass")
	} else {
		fmt.Printf("%+v", err)
	}
}
複製代碼

上述代碼可在ksloveyuan/rsautil查看。

  1. 接着,我用echo寫了一個簡單的server端。
package main

import (
	"github.com/ksloveyuan/rsautil"
	"net/http"
	"github.com/labstack/echo"
)

const PublicKey  = `-----BEGIN PUBLIC KEY----- 公鑰 -----END PUBLIC KEY-----``

type VerifyArgs struct {
	Content string  `json:"content" description:"" binding:"required" `
	Signature string  `json:"signature" description:"" binding:"required" `
}

func main() {
	e := echo.New()

	e.POST("/verify", func(c echo.Context) error {
		args:= VerifyArgs{}
		if err := c.Bind(&args); err !=nil{
			return c.String(http.StatusBadRequest, "參數不正確")
		}

		message := ""
		if success, _ := rsautil.VerySignWithSha256Base64(args.Content, args.Signature, []byte(PublicKey)); success{
			message = "success"
		} else {
			message = "fail"
		}

		return c.String(http.StatusOK, message)
	})

	e.Logger.Fatal(e.Start(":1323"))
}
複製代碼

前端實現

前端的主要工做是發起請求,同時附帶請求參數的簽名。

let crypto = require('crypto')
let request = require('request')

let sk = `-----BEGIN RSA PRIVATE KEY-----對應的私鑰-----END RSA PRIVATE KEY-----`

function sendRequest ({ content, signature }) {
    var bodyData = { content, signature }
    return new Promise((resolve, reject) => {
        request.post({ url: 'http://localhost:1323/verify', body: bodyData, json: true }, function optionalCallback (
            err,
            httpResponse,
            body
        ) {
            if (err) {
                reject()
                return console.error('upload failed:', err)
            }

            console.log('Upload successful!', body)
            resolve()
        })
    })
}

async function action() {
    let signer = crypto.createSign('RSA-SHA256')

    let content = 'test_test_test'
    signer.update(content)

    let privateKey = {key: sk, format:"pem", type:"pkcs1"}
    let signature = signer.sign(privateKey, 'base64')

    console.log(signature)

    await sendRequest({ content, signature })
}

action()

複製代碼

其中request第三方包,crypto是nodejs自帶的庫。

代碼中須要注意的地方有兩點:

  1. 選用的簽名方法必定要是RSA-SHA256,不然的話,和後端就對不上了。
  2. 使用的私鑰的格式參數雖然是默認參數,但最好顯示指定哈。

以上demo的完整代碼可在ksloveyuan/ApiSecurityDemo中查看,歡迎star哈。

寫在最後

關於私鑰的保存,明文HardCode在代碼裏天然是一個安全隱患。

這一點,一方面能夠保存在文件裏,使用時讀取,密鑰的安全由保管的人負責(話說ssh密鑰登陸就是這樣);或者是對私鑰再作一層AES的加密,每次使用時輸入AES加密的Keyword。

話說回來,安全攻防是沒有盡頭的,主要仍是要看要保證的安全級別而定。

公司前端的同事,對目前的安全保證已經很滿意了。

相關文章
相關標籤/搜索