bsn-sdk操作文档

414 阅读12分钟

本示例参照官方文档

链码开发

  • 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