本示例参照官方文档
链码开发
- www.bsnbase.com/static/base/BaseChainCode.zip
签名算法
1、将 userCode+ appCode+ chainCode+ funcName 的值以及 args 中每一项数据拼接
成字符串 A;
2、对字符串 A 使用用户证书的私钥进行 SHA256WITHECDSA 签名。
请求参数
| 序号 | 字段名 | 字段 | 类型 | 必填 | 备注 |
|---|---|---|---|---|---|
| 1 | 信息头 | header | Map | 是 | |
| 2 | 信息体 | body | Map | 是 | |
| 3 | 签名值 | mac | String | 是 | |
| header | |||||
| 1 | 用户唯一标识 | userCode | String | 是 | |
| 2 | 应用唯一标识 | appCode | String | 是 | |
| body | |||||
| 1 | 链码code | chainCode | String | 是 | |
| 2 | 方法名称 | funcName | String | 是 | |
| 3 | 请求参数 | args | String[] | 否 |
示例
{
"header":{
"appCode":"CL1881038873220190902114314",
"userCode":"newuser",
"tId":""
},
"body":{
"args":[
"16399b5085ee5d3981f5076c33c5a0a66d7f2f3545b4d88501116a8bd53d13a5",
"{"fileName":"test.jpg","fileHash":"6cfacf57e5b27f71a47a812938021784"}"
],
"funcName":"set",
"chainCode":"cc_bcj"
},
"mac":"MEUCIQDTFe2Gerdf7YJrG1a1Yt99M0ZQ3T1lGpsXdNmFV7WuTgIgSkZ19abUhAJbMrJMBoD8N7f26xhp
QRuR4vNAfY7EEbs="
}
签名值:
newuserCL1881038873220190902114314cc_bcjset16399b5085ee5d3981f5076c33c5a0a66d7f2f35
45b4d88501116a8bd53d13a5{"fileName":"test.jpg","fileHash":"6cfacf57e5b27f71a47a81293802
1784"}
注:签名值:加密对象:userCode+ appCode+ chainCode+ funcName+args[0]+args[1]+…+args[args.length-1]
私钥:通过证书下载获取的私钥
用户与应用 ID:用户参与应用后的关联 ID
请求参数:是否为空由具体的业务场景决定,可以为空
响应参数
| 序号 | 字段名 | 字段 | 类型 | 必填 | 备注 |
|---|---|---|---|---|---|
| 1 | 信息头 | header | Map | 是 | |
| 2 | 信息体 | body | Map | 是 | |
| 3 | 签名值 | mac | String | 是 | |
| header | |||||
| 1 | 响应标识 | code | int | 是 | 0: 校验成功 -1: 校验失败 |
| 2 | 响应信息 | msg | String | 否 | code==0时可以为nil |
| body | |||||
| 1 | 块信息 | blockInfo | Map | 否 | code不为0时为空 |
| 2 | 链码响应结果 | ccRes | Map | 否 | code不为0时为空 |
| blockInfo | |||||
| 1 | 交易ID | txId | String | 是 | |
| 2 | 状态值 | status | int | 是 | 0: 提交成功且落块, -1:失败 |
| ccRes | |||||
| 1 | 链码响应状态 | ccCode | int | 是 | 200:成功 500:失败 |
| 2 | 链码响应结果 | args | String | 否 | 具体看链码的响应结果 |
示例
{
"header":{
"code":0,
"msg":"处理成功"
},
"body":{
"blockInfo":{
"txId":"62ef371fe90cda1586c6757b924aaa48cbd9d2ec057325ebf30df052e8fa6134",
"status":0
},
"ccRes":{
"ccCode":200,
"ccData":"保存版权信息成功"
}
},
"mac":"MEQCID27XE05k2gN71s2R94CfWZ79L6BG7daN3uDNvzBnk4IAiACR/05PKJMDAHpOurMTjC1KGiHeeZK
hmdARU47CIUelg=="
}
Mac 验签
在门户中下载证书时,会下载相应网关的公钥证书,需要使用该证书验签
签名值:
newuserCL1881038873220190902114314cc_bcjset16399b5085ee5d3981f5076c33c5a0a66d7f2f35
45b4d88501116a8bd53d13a5{"fileName":"test.jpg","fileHash":"6cfacf57e5b27f71a47a81293802
1784"}
业务开发
环境准备
一般的go,fabric等不再赘述,这里讲一下bsn-sdk-go如何导入
因为目前没发现官方在github上开repo,所以有两种方式:
- 自己手动下载并放置到GOPATH的src下
- 可以自行开一个repo用go get的方式(缺点就是如果官方更新sdk这个repo就废掉了)
- 第一步将bsn-sdk-go上传到一个git repo下
- 第二步go get .../bsn-sdk-go
项目结构
- Certs:主要用于存放用户的公私证书、网关的公钥证书以及请求网关时所需要的Https 公钥证书
- bsngate_https.crt:节点网关 API 的 Https 公钥证书(用于对请求网关 API 地址所需加载的公钥证书)
- gateway_public_cert.crt:网关公钥证书(用于对网关响应的数据采用椭圆曲线算法进行验签)
- private_key.pem:用户私钥证书(用于对请求网关的数据采用椭圆曲线算法进行签名)
- public_cert.pem:用户公钥证书(目前没有用到)
- common:主要用于存放公共库,本示例中用于对 ecdsa 椭圆曲线算法工具类进行定义
- ecdsa.go:用于对 ecdsa 椭圆曲线算法工具类进行定义
- model :主要用于对请求网关和网关的响应报文数据结构进行定义
- request.go :用于对请求节点网关 API 的数据报文进行定义
- response.go:用于对节点网关 API 响应的数据报文进行定义
- main.go:main 文件是示例程序的入口以及包含调用节点网关 API 的相关业务逻辑代码。
流程说明
- 修改调用网关所对应的请求参数
- 拼接待签名的字符串,对字符串使用用户私钥证书进行 SHA256WITHECDSA 签名加密(调用 ecdsa.go 下的 SignECDSA 方法进行签名,并生成 base64 格式的 mac 值)
- 发起 post 请求,并且附加 HTTPS 证书
- 获取返回报文中的 mac 值,对返回报文中的 mac 值,使用网关的公钥证书进行验签,验签内容与传参时签名字符串相同
- 并将验证结果输出到控制台
函数入口
main.go
/**
* @Author: Gao Chenxi
* @Description:
* @Date: 2020/4/1 4:35 PM
* @File: main
*/
package main
import (
"bytes"
"crypto/ecdsa"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"path"
"path/filepath"
)
func main() {
dirPath, err := filepath.Abs(".")
if err != nil {
fmt.Println("获取目录失败", err.Error())
return
}
// TODO
privateKey, err := LoadPrivateKey(path.Join(dirPath, "userPrivateKey"))
if err != nil {
fmt.Println("读取用户私钥失败:", err.Error())
return
}
// TODO
publicKey, err := LoadPublicKeyByFile(path.Join(dirPath, "gatewayPublicCert"))
if err != nil {
fmt.Println("读取网关公钥失败:", err.Error())
return
}
upload(privateKey, publicKey, "set", "dc1d6010b7ff421dae0146f193dded01",
"CL1851016378620191011150510")
upload(privateKey, publicKey, "update", "dc1d6010b7ff421dae0146f193dded01",
"AL1851016378620191011150510")
upload(privateKey, publicKey, "get", "dc1d6010b7ff421dae0146f193dded01")
upload(privateKey, publicKey, "delete", "dc1d6010b7ff421dae0146f193dded01")
}
/**
* @Author AndyCao
* @Date 2019-10-11 20:05
* @Description 开始进行数据上链/修改/获取/删除
* @Param privateKey 用户私钥
* @return
**/
func upload(privateKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey, method string, key string, value ...string) {
switch method {
case "set":
fmt.Println("开始上链")
break
case "update":
fmt.Println("开始对链数据进行修改")
break
}
reqBody := ReqContent{
Header: ReqHeader{
UserCode: "reddate",
AppCode: "",
TId: "",
},
Body: ReqBody{
Chaincode: "cc_base",
FuncName: method,
},
}
if method == "set" || method == "update" {
reqBody.Body.Args = []string{fmt.Sprintf("{\"baseKey\":\"%s\",\"baseValue\":\"%s\"}", key, value)}
} else {
reqBody.Body.Args = []string{fmt.Sprintf("%s", key)}
}
var buffer bytes.Buffer
buffer.WriteString(reqBody.Header.UserCode)
buffer.WriteString(reqBody.Header.AppCode)
buffer.WriteString(reqBody.Body.Chaincode)
buffer.WriteString(reqBody.Body.FuncName)
for _, value := range reqBody.Body.Args {
buffer.WriteString(value)
}
digest := GetSHA256HASH(buffer.String())
// 对哈希值获取签名
sign, err := SignECDSA(privateKey, digest)
if err != nil {
fmt.Println("签名异常:", err.Error())
return
}
// base64编码
reqBody.Mac = base64.StdEncoding.EncodeToString(sign)
// 序列化
reqBytes, err := json.Marshal(reqBody)
if err != nil {
fmt.Println("请求内容序列化失败:", err.Error())
return
}
resBytes, err := sendPost(reqBytes)
if err != nil {
return
}
var resBody = ResContent{}
err = json.Unmarshal(resBytes, &resBody)
if err != nil {
fmt.Println("响应结果数据反序列化失败:", err.Error())
}
if resBody.Header.Code == 0 && resBody.Body.CCRes.CCCode == 200 && resBody.Body.BlockInfo.Status == 0 {
// 针对响应的Mac签名值进行Base64解码
resSign, err := base64.StdEncoding.DecodeString(resBody.Mac)
if err != nil {
fmt.Println("响应数据解码失败:", err.Error())
}
// 开始验签
result, err := VerifyECDSA(publicKey, resSign, digest)
if result {
fmt.Println(fmt.Sprintf(" 验 签 成 功 , 交 易 ID : 【 %s 】 , 交 易 状 态 : 【 %d 】 ",
resBody.Body.BlockInfo.TxId, resBody.Body.BlockInfo.Status))
} else {
fmt.Println("验签失败!")
}
}
}
func sendPost(dataBytes []byte) ([]byte, error) {
//获取项目目录
dirPath, err := filepath.Abs(".")
if err != nil {
fmt.Println("获取当前目录失败:", err.Error())
return nil, err
}
// 读取 https 证书内容
// TODO
caCert, err := ioutil.ReadFile(path.Join(dirPath, "httpsPublicCert"))
if err != nil {
fmt.Println("读取 https 证书内容失败:", err.Error())
return nil, err
}
//构建证书池
caCertPool := x509.NewCertPool()
//将读取的 https 证书内容添加到证书池
caCertPool.AppendCertsFromPEM(caCert)
//构建 http 请求客户端
client := &http.Client{
//定义单个 HTTP 请求的机制
Transport: &http.Transport{
//定义 TLS 客户端配置
TLSClientConfig: &tls.Config{
//添加 RootCA 证书池(此处将 https 的公钥证书添加到 RootCA 证书池中)
RootCAs: caCertPool,
},
},
}
//调用接口
fmt.Println("请求报文:", string(dataBytes))
// TODO
response, err := client.Post("nodeApiUrl", "application/json",
bytes.NewReader(dataBytes))
if err != nil {
fmt.Println("请求节点网关 API 出现异常:", err.Error())
return nil, err
}
//从响应对象获取响应报文数据,并进行读取
bytes := make([]byte, response.ContentLength)
response.Body.Read(bytes)
fmt.Println("响应报文:", string(bytes))
return bytes, nil
}
节点网关请求数据结构
model/request.go
package main
type ReqContent struct {
Header ReqHeader `json:"header"`
Body ReqBody `json:"body"`
Mac string `json:"mac"`
}
type ReqHeader struct {
UserCode string `json:"user_code"`
AppCode string `json:"app_code"`
TId string `json:"t_id"`
}
type ReqBody struct {
Chaincode string `json:"chaincode"`
FuncName string `json:"func_name"`
Args []string `json:"args"`
}
节点网关响应数据结构
model/response.go
package main
type ResContent struct {
Header ResHeader `json:"header"`
Body ResBody `json:"body"`
Mac string `json:"mac"`
}
type ResHeader struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
type ResBody struct {
BlockInfo BlockInfo `json:"block_info"`
CCRes CCRes `json:"cc_res"`
}
type BlockInfo struct {
Status int `json:"status"`
TxId string `json:"tx_id"`
}
type CCRes struct {
CCCode int `json:"cc_code"`
CCData string `json:"cc_data"`
}
数据签名、验签
common/ecdsa.go
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"math/big"
)
type ECDSASignature struct {
R, S *big.Int
}
var (
//标准椭圆曲线(curvehalfOrders 包含预计算的曲线组顺序减半,用于确保签名的值小于或等于
//曲线组顺序减半。我们只接受低 S 签名。它们是为了提高效率而预先计算的)
curveHalfOrders = map[elliptic.Curve]*big.Int{
elliptic.P224(): new(big.Int).Rsh(elliptic.P224().Params().N, 1),
elliptic.P256(): new(big.Int).Rsh(elliptic.P256().Params().N, 1),
elliptic.P384(): new(big.Int).Rsh(elliptic.P384().Params().N, 1),
elliptic.P521(): new(big.Int).Rsh(elliptic.P521().Params().N, 1),
}
)
/**
* @Author AndyCao
* @Date 2019-10-12 11:38
* @Description 使用公钥对象检查 S 是否为低 S
* @Param k 公钥对象
* @Param s 待检查 s
* @return 返回判断结果
**/
func IsLowS(k *ecdsa.PublicKey, s *big.Int) (bool, error) {
halfOrder, ok := curveHalfOrders[k.Curve]
if !ok {
return false, fmt.Errorf("curve not recognized [%s]", k.Curve)
}
return s.Cmp(halfOrder) != 1, nil
}
/**
* @Author AndyCao
* @Date 2019-10-12 11:48
* @Description 使用公钥对象待转换 S 进行低 S 转换
* @Param k 公钥对象
* @Param s 待转换 S
* @return 返回转换后的低 S
**/
func ToLowS(k *ecdsa.PublicKey, s *big.Int) (*big.Int, bool, error) {
lowS, err := IsLowS(k, s)
if err != nil {
return nil, false, err
}
if !lowS {
// Set s to N - s that will be then in the lower part of signature space
// less or equal to half order
s.Sub(k.Params().N, s)
return s, true, nil
}
return s, false, nil
}
/**
* @Author AndyCao
* @Date 2019-10-12 11:34
* @Description 字节数组与整型的转换
* @Param raw 字节数组
* @return 转换后的整型数据
**/
func UnmarshalECDSASignature(raw []byte) (*big.Int, *big.Int, error) {
// Unmarshal
sig := new(ECDSASignature)
_, err := asn1.Unmarshal(raw, sig)
if err != nil {
return nil, nil, fmt.Errorf("failed unmashalling signature [%s]", err)
}
// 验证 SIG
if sig.R == nil {
return nil, nil, errors.New("invalid signature, R must be different from nil")
}
if sig.S == nil {
return nil, nil, errors.New("invalid signature, S must be different from nil")
}
if sig.R.Sign() != 1 {
return nil, nil, errors.New("invalid signature, R must be larger than zero")
}
if sig.S.Sign() != 1 {
return nil, nil, errors.New("invalid signature, S must be larger than zero")
}
return sig.R, sig.S, nil
}
/**
* @Author AndyCao
* @Date 2019-10-12 11:49
* @Description 获取曲线半阶
* @Param c 曲线对象
* @return 返回获取的曲线半阶
**/
func GetCurveHalfOrdersAt(c elliptic.Curve) *big.Int {
return big.NewInt(0).Set(curveHalfOrders[c])
}
/**
* @Author AndyCao
* @Date 2019-10-12 11:31
* @Description 使用公钥对象,以及原待签名数据对签名数所进行验签
* @Param k 公钥对象
* @Param signature 签名数据
* @Param digest 原待签名数据
* @return 返回验签结果
**/
func VerifyECDSA(k *ecdsa.PublicKey, signature, digest []byte) (bool, error) {
r, s, err := UnmarshalECDSASignature(signature)
if err != nil {
return false, fmt.Errorf("Failed unmashalling signature [%s]", err)
}
lowS, err := IsLowS(k, s)
if err != nil {
return false, err
}
if !lowS {
return false, fmt.Errorf("Invalid S. Must be smaller than half the order [%s][%s].",
s, GetCurveHalfOrdersAt(k.Curve))
}
return ecdsa.Verify(k, digest, r, s), nil
}
/**
* @Author AndyCao
* @Date 2019-10-12 11:20
* @Description 根据私钥文件路径获取私钥数据并构建私钥对象
* @Param file 私钥文件路径
* @return 返回私钥对象
**/
func LoadPrivateKeyByFile(file string) (*ecdsa.PrivateKey, error) {
b, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
bl, _ := pem.Decode(b)
if bl == nil {
return nil, errors.New("failed to decode PEM block from " + file)
}
key, err := x509.ParsePKCS8PrivateKey(bl.Bytes)
if err != nil {
return nil, errors.New("failed to parse private key from " + file)
}
return key.(*ecdsa.PrivateKey), nil
}
/**
* @Author AndyCao
* @Date 2019-10-12 11:20
* @Description 根据私钥数据构建私钥对象
* @Param privateKey 私钥数据内容
* @return 返回私钥对象
**/
func LoadPrivateKey(privateKey string) (*ecdsa.PrivateKey, error) {
bl, _ := pem.Decode([]byte(privateKey))
if bl == nil {
return nil, errors.New("failed to decode PEM block from PrivateKey")
}
key, err := x509.ParsePKCS8PrivateKey(bl.Bytes)
if err != nil {
return nil, errors.New("failed to parse private key from PrivateKey")
}
return key.(*ecdsa.PrivateKey), nil
}
/**
* @Author AndyCao
* @Date 2019-10-12 11:22
* @Description 根据公钥文件路径获取公钥数据并构建公钥对象
* @Param file 公钥文件路径
* @return 返回公钥对象
**/
func LoadPublicKeyByFile(file string) (*ecdsa.PublicKey, error) {
b, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
bl, _ := pem.Decode(b)
if bl == nil {
return nil, errors.New("failed to decode PEM block from " + file)
}
key, err := x509.ParseCertificate(bl.Bytes)
if err != nil {
return nil, errors.New("failed to parse private key from " + file)
}
return key.PublicKey.(*ecdsa.PublicKey), nil
}
/**
* @Author AndyCao
* @Date 2019-10-12 11:25
* @Description 根据公钥数据构建公钥对象
* @Param cert 公钥数据内容
* @return 返回公钥对象
**/
func LoadPublicKey(cert string) (*ecdsa.PublicKey, error) {
bl, _ := pem.Decode([]byte(cert))
if bl == nil {
return nil, errors.New("failed to decode PEM block from Certificate")
}
key, err := x509.ParseCertificate(bl.Bytes)
if err != nil {
return nil, errors.New("failed to parse private key from Certificate")
}
return key.PublicKey.(*ecdsa.PublicKey), nil
}
/**
* @Author AndyCao
* @Date 2019-10-11 14:10
* @Description 使用私钥对象待签名数字数组进行签名
* @Param k 私钥对象
* @Param digest 待签名数字数组
* @return 返回签名后数据
**/
func SignECDSA(k *ecdsa.PrivateKey, digest []byte) (signature []byte, err error) {
r, s, err := ecdsa.Sign(rand.Reader, k, digest)
if err != nil {
return nil, err
}
s, _, err = ToLowS(&k.PublicKey, s)
if err != nil {
return nil, err
}
return marshalECDSASignature(r, s)
}
/**
* @Author AndyCao
* @Date 2019-10-12 11:27
* @Description 整型到字节数组的转换
* @Param r, s 签名后的数据
* @return 转换后的字节数组
**/
func marshalECDSASignature(r, s *big.Int) ([]byte, error) {
return asn1.Marshal(ECDSASignature{r, s})
}
/**
* @Author AndyCao
* @Date 2019-10-11 14:10
* @Description 获取数据哈希值
* @Param data 待处理数据
* @return 返回获取后的数据哈希值
**/
func GetSHA256HASH(data string) []byte {
bmsg := []byte(data)
h := sha256.New()
h.Write([]byte(bmsg))
hash := h.Sum(nil)
return hash
}
Java部分
至于request和response的结构等和go-sdk一致不做补充,只将一下目录结构和业务系统开发流程。
业务开发
目录结构
- java:
- controller: 控制器
- core:工具处理类(包含https请求,dto数据传输对象,枚举类,异常处理机制,数据签名与验签)
- model:数据模型
- service:业务逻辑处理
- resource:
- application.yaml:配置信息
- cert:私钥
- https:网关证书
- webapp:
- 前端页面css,js
流程说明
客户端填写上链信息--->controller(数据处理,报文封装,数据签名)--->请求节点网络--->网关响应报文验签--->响应
详细说明
- 获取用户私钥证书等拷贝到项目中,在配置文件中配置路径
- https请求工具类封装处理
- 报文签名与响应报文验签工具 (其实和go-sdk一致,就是将官方的包导入,再拼装请求等操作)
参考
如何使用BSN
// TODO