관리 메뉴

HAMA 블로그

400라인의 go코드로 구현한 하이퍼레저 패브릭 [4-1]- MSP (1) 본문

블록체인

400라인의 go코드로 구현한 하이퍼레저 패브릭 [4-1]- MSP (1)

[하마] 이승현 (wowlsh93@gmail.com) 2019. 2. 25. 18:43

MSP는 하이퍼레저 패브릭에서 각 피어와 사용자에 대한 인증/인가 작업에 대한 추상층입니다. 퍼블릭 블록체인과는 다른 콘소시엄 블록체인에서만 복잡하게 존재하는 모듈로써,  구현하는거 자체도 PKI의 복잡성을 그대로 물려받기 때문에 복잡하지만 실제 콘소시엄 블록체인을 구축하여 조직들간의 거버넌스 정책을 만들어 나가는 프로세스는 더욱 더 복잡하지 않을 까 싶습니다. 즉 새로운 조직을 어떻게 추가시키는지 같은? 현재 대부분의 패브릭 프로젝트에서 설립자 주도적 네트워크 구성을 하고 있는것으로 아는데 (즉 조직간 합의가 필요없음. 그냥 짱이 알아서 하는?) 진짜 조직별로 권한이 균등이 나누어져 있는 네트워크에서의 실제 사용사례에 대한 레퍼런스에 대한 공개가 기대됩니다.

이 글에서는 하이퍼레저 패브릭에서 사용되는 MSP를 간단하게 구현해보는 시간을 갖으려 합니다. 해싱/암호화/PKI 등에 대한 기본지식은 알고 있다고 가정하며(사실 읽으면서 찾아보면 됩니다) 첫번째로는 각 기술에 대한 코딩 지식 기반과 무엇을 할 수 있는지에 대한 감을 잡고, 후에는 실제 하이퍼레저패브릭 심플 시뮬레이션을 만들어서 피어계열인증서/사용자계열인 증서 기반의 트랜잭션을 일으키고 오더러에서 확인하는 과정을 코드로써 살펴 볼 것 입니다.  

Go언어의 Crypto라이브러리 

go 언어가 지원하는 표준 라이브러리들 입니다. 이를 이용한 여러가지 암호화 관련 된 코드는 더 아래에서 살펴보죠. 

aes : Package aes implements AES encryption (formerly Rijndael), as defined in U.S. Federal Information Processing Standards Publication 197.
cipher:  Package cipher implements standard block cipher modes that can be wrapped around low-level block cipher implementations.
des: Package des implements the Data Encryption Standard (DES) and the Triple Data Encryption Algorithm (TDEA) as defined in U.S. Federal Information Processing Standards Publication 46-3.
dsa: Package dsa implements the Digital Signature Algorithm, as defined in FIPS 186-3.
ecdsa: Package ecdsa implements the Elliptic Curve Digital Signature Algorithm, as defined in FIPS 186-3.
elliptic: Package elliptic implements several standard elliptic curves over prime fields.
hmac: Package hmac implements the Keyed-Hash Message Authentication Code (HMAC) as defined in U.S. Federal Information Processing Standards Publication 198.
md5: Package md5 implements the MD5 hash algorithm as defined in RFC 1321.
rand: Package rand implements a cryptographically secure pseudorandom number generator.
rc4: Package rc4 implements RC4 encryption, as defined in Bruce Schneier’s Applied Cryptography.
rsa: Package rsa implements RSA encryption as specified in PKCS#1.
sha1: Package sha1 implements the SHA1 hash algorithm as defined in RFC 3174.
sha256: Package sha256 implements the SHA224 and SHA256 hash algorithms as defined in FIPS 180-4.
sha512: Package sha512 implements the SHA-384, SHA-512, SHA-512/224, and SHA-512/256 hash algorithms as defined in FIPS 180-4.
subtle: Package subtle implements functions that are often useful in cryptographic code but require careful thought to use correctly.
tls: Package tls partially implements TLS 1.2, as specified in RFC 5246.
x509: Package x509 parses X.509-encoded keys and certificates.
pkix: Package pkix contains shared, low level structures used for ASN.1 parsing and serialization of X.509 certificates, CRL and OCSP. 

X.509 인증서 구조 

자신의 공개키를 인증 받기 위해 CA의 비밀키를 이용해서 만든 CA서명증서입니다.검증은 CA셀프인증서를 통해서 할 수 있습니다. 구체적으로는 CA(or Intermediate CA) 공개키로 위의 그림에서 서명알고리즘으로 복호화 한것과 서명이 일치하면 OK란거죠. 서명알고리즘에는 RSA계열과 ECDSA계열이 있습니다.


Go언어를 이용한 암호화 코딩 예제 

0. HMAC 을 이용한 메세지 해싱 

HMAC:  hash message authentication code  즉 해싱에 의한 메세지 검증/인증으로 비밀키에 의해  -> 해싱은 되나  <- 반대 방향의 복구는 불가능한 , 즉 한방향으로만 작동하기 때문에 보통 확인 용도(보낸 내용이 무엇인지가 아니라, 보낸 내용이 보낼 때와 변경되진 않았는지 or 이미 저장되어 있는 정보와 일치하는지) 로 사용됩니다. 또한해싱의 특성상 원래의 것 보다 더 짧은 길이의 값이나 키로 변환해서 이득을 얻을 수 있는 많은 곳에 사용됩니다.

"crypto/hmac"
"crypto/sha256"
"crypto/sha512"


func main() {
message := "Today web engineering has modern apps ..."
salt := generateSalt()
fmt.Println("Message: " + message)
fmt.Println("\nSalt: " + salt)

hash := hmac.New(sha256.New, []byte(secretKey))
io.WriteString(hash, message+salt)
fmt.Printf("\nHMAC-Sha256: %x", hash.Sum(nil))

hash = hmac.New(sha512.New, []byte(secretKey))
io.WriteString(hash, message+salt)
fmt.Printf("\n\nHMAC-sha512: %x", hash.Sum(nil))
}


1. AES 대칭키를 이용한 암호화/복호화 

서로 공유하고 있는 동일한 키(대칭키라고 함)로 암호화하고 복호화 하는 방식에 관한 코드입니다.

"crypto/aes"


func encrypt(key []byte, text string) (string, error) {
block, err := aes.NewCipher(key)
cfb := cipher.NewCFBEncrypter(block, iv)
... }

func decrypt(key []byte, text string) (string, error) {
block, err := aes.NewCipher(key)
cfb := cipher.NewCFBDecrypter(block, iv)
...
}


2. RSA 개인키/공개키 생성

 RSA 기반의 개인키와 공개키를 생성합니다. 대칭키와 다르게 2가지의 키가 생성됩니다.


rsaPriKey, rsaPubKey := GenerateKeyPair(1024) // 개인키와 공개키 쌍 생성


3. PEM EXPORT / IMPORT 

 개인키,공개키 혹은 인증서등을 pem 파일로 export/import 할 수 있습니다.여기서는 확장자를 pem으로 하였지만 crt (인증서) , key (개인키) 등으로 할 수도 있습니다.
func main(){

   // GENERATE
   privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
   publicKey := &privateKey.PublicKey

   // WRITE to pem file 
   pemPrivateFile, err := os.Create("private_key.pem")
   if err != nil {
      fmt.Println(err)
      os.Exit(1)
   }

   var pemPrivateBlock = &pem.Block{
      Type:  "RSA PRIVATE KEY",
      Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
   }

   err = pem.Encode(pemPrivateFile, pemPrivateBlock)
   if err != nil {
      fmt.Println(err)
      os.Exit(1)
   }
   pemPrivateFile.Close()

   // READ From pem file 
   privateKeyFile, err := os.Open("private_key.pem")
   if err != nil {
      fmt.Println(err)
      os.Exit(1)
   }

   pemfileinfo, _ := privateKeyFile.Stat()
   size := pemfileinfo.Size()
   pembytes := make([]byte, size)
   buffer := bufio.NewReader(privateKeyFile)
   _, err = buffer.Read(pembytes)
   data, _ := pem.Decode([]byte(pembytes))
   privateKeyFile.Close()

   privateKeyImported, err := x509.ParsePKCS1PrivateKey(data.Bytes)
  
}


4. RSA 암호화/복호화

 hello를 공개키로 암호화하고, 개인키로 복호화 합니다. (키쌍을 암호화에 사용) 

////////// Encrypt / Decrypt ///////

rsaPriKey, rsaPubKey := GenerateKeyPair(1024)  // 개인키와 공개키 쌍 생성

encryptedMsg := EncryptWithPublicKey([] byte ("hello"), rsaPubKey2 ) // 공개키로 암호화
decryptedMsg := DecryptWithPrivateKey(encryptedMsg, rsaPriKey2) // 개인키로 복호화 


5. RSA 서명/확인

 hello를 개인키로 서명하고, 공개키로 확인 합니다. (키 쌍을 서명을 위해 사용 + 자기부인 방지)

////////// Sign / Verify  ////////////

alicePrivateKey, err := rsa.GenerateKey(rand.Reader, 2048) // 개인키와 공개키 쌍 생성 
alicePublicKey := alicePrivateKey.PublicKey

secretMessage := "Hello 8gwifi.org"

signature := SignPKCS1v15(secretMessage,*alicePrivateKey) // 개인키로 서명  
verif := VerifyPKCS1v15(signature, secretMessage,  alicePublicKey ) // 공개키로 확인 


6. ECDSA 개인키/공개키 생성 

 하이퍼레즈 패브릭이나 이더리움에서는 RSA 말고 ECDSA를 사용합니다.

pubkeyCurve := elliptic.P256() //see http://golang.org/pkg/crypto/elliptic/#P256

privatekey := new(ecdsa.PrivateKey)
privatekey, err := ecdsa.GenerateKey(pubkeyCurve, rand.Reader)

if err != nil {
   fmt.Println(err)
   os.Exit(1)
}

var pubkey ecdsa.PublicKey
pubkey = privatekey.PublicKey


7. ECDSA 서명/확인

RSA에서와 마찬가지로 서명/확인에 사용 할 수 있습니다.
pubkeyCurve := elliptic.P256() //see http://golang.org/pkg/crypto/elliptic/#P256

privatekey := new(ecdsa.PrivateKey)
privatekey, err := ecdsa.GenerateKey(pubkeyCurve, rand.Reader) 

var pubkey ecdsa.PublicKey
pubkey = privatekey.PublicKey

// Sign ecdsa style
var h hash.Hash
h = md5.New()
r := big.NewInt(0)
s := big.NewInt(0)

io.WriteString(h, "This is a message to be signed and verified by ECDSA!")
signhash := h.Sum(nil)

r, s, serr := ecdsa.Sign(rand.Reader, privatekey, signhash)

signature := r.Bytes()
signature = append(signature, s.Bytes()...)

// Verify
verifystatus := ecdsa.Verify(&pubkey, signhash, r, s)
fmt.Println(verifystatus) 


8. ECDH 로 (비밀키 전송없이) 비밀키 공유하기

간단히 말해서 나의 비밀키 * 상대방의 공개키 == 상대방의 비밀키 * 나의 공개키 라는 등식을 이용해서, 공통의 비밀키를 갖는 방식입니다. 이더리움에서는 피어끼리 접속 할 때마다 임시의 공개키/개인키를 만들어서 (참고로 임시가 아닌 static 공개키는 바로 자신의 주소죠) ECDH를 이용하여 만든 비밀키로 RLP인코딩한 패킷을 암호화하여 송/수신합니다. 
var privKey1, privKey2 crypto.PrivateKey
var pubKey1, pubKey2 crypto.PublicKey

var err error
var ok bool
var secret1, secret2 []byte

privKey1, pubKey1, err = e.GenerateKey(rand.Reader)
privKey2, pubKey2, err = e.GenerateKey(rand.Reader)


secret1, err = e.GenerateSharedSecret(privKey1, pubKey2)
secret2, err = e.GenerateSharedSecret(privKey2, pubKey1)

if !bytes.Equal(secret1, secret2) {
   log.Print("The two shared keys: %d, %d do not match", secret1, secret2)
}


9. PKI 시스템 (CA생성,서명하기,서명 확인하기) 


웹서비스 회사에서 https 서비스를 하기 위해 Verisign, Comodo, Thawte, GeoTrust, GlobalSign 같은 CA로 부터 자신의 공개키를 그들의 개인키로 서명한 인증서(. X.509)를 받아서 사용합니다. 이렇게 CA를 통하여 인증을 받은 공개키를 통해 신원을 검증하고 비밀키 교환을 하는 일련의 과정에 인프라를 PKI라고 합니다. 

하이퍼레저 패브릭 같은 콘소시엄형 블록체인은 각 조직에 대한 허가/인증이 중요한 기능이기에 이런 PKI를 시스템내에 직접 구현하여 활용합니다. 먼가 복잡해 보이지만  개인키,공개키 만드는 로직/인증서 만드는 로직/ 인증서 검증하는 로직만 있으면 만들 수 있습니다. 
- CA 자가서명 (CA는 루트이므로 자기 스스로 서명하게 됩니다.)
func main() {
   ca := &x509.Certificate{
      SerialNumber: big.NewInt(1653),
      Subject: pkix.Name{
         Organization:  []string{"ORGANIZATION_NAME"},
         Country:       []string{"COUNTRY_CODE"},
         Province:      []string{"PROVINCE"},
         Locality:      []string{"CITY"},
         StreetAddress: []string{"ADDRESS"},
         PostalCode:    []string{"POSTAL_CODE"},
      },
      NotBefore:             time.Now(),
      NotAfter:              time.Now().AddDate(10, 0, 0),
      IsCA:                  true,
      ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
      KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
      BasicConstraintsValid: true,
   }


   privateKey, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) // ecdsa.PrivateKey
   publicKey := &privateKey.PublicKey // ecdsa.PublicKey

   ca_b, err := x509.CreateCertificate(rand.Reader, ca, ca, publicKey, privateKey)

   if err != nil {
      log.Println("create ca failed", err)
      return
   }

   // Public key
   certOut, err := os.Create("ca.crt")
   pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: ca_b})
   certOut.Close()
   log.Print("written cert.pem\n")

   // Private key
   keyOut, err := os.OpenFile("ca.key", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
   x509Encoded, _ := x509.MarshalECPrivateKey(privateKey)
   pem.Encode(keyOut, &pem.Block{Type: "ECDSA PRIVATE KEY", Bytes: x509Encoded})
   keyOut.Close()
   log.Print("written key.pem\n")
}

- CA를 이용한 개인 인증서 생성 및 확인 (개인들은 자신의 공개키/개인키를 만든 후에 공개키를 CA의 개인키를 이용하여 서명하고 X.509구조의 인증서를 만들게 됩니다) 
package main

import (
   "bufio"
   "crypto/ecdsa"
   "crypto/elliptic"
   "crypto/rand"
   "crypto/tls"
   "crypto/x509"
   "crypto/x509/pkix"
   "encoding/pem"
   "fmt"
   "log"
   "math/big"
   "os"
   "time"
)

func main() {

   // Load CA
   catls, err := tls.LoadX509KeyPair("ca.crt", "ca.key")
   if err != nil {
      panic(err)
   }
   ca, err := x509.ParseCertificate(catls.Certificate[0])
   if err != nil {
      panic(err)
   }

   cert := &x509.Certificate{
      SerialNumber: big.NewInt(1653),
      Subject: pkix.Name{
         Organization:  []string{"ORGANIZATION_NAME"},
         Country:       []string{"COUNTRY_CODE"},
         Province:      []string{"PROVINCE"},
         Locality:      []string{"CITY"},
         StreetAddress: []string{"ADDRESS"},
         PostalCode:    []string{"POSTAL_CODE"},
      },
      NotBefore:             time.Now(),
      NotAfter:              time.Now().AddDate(10, 0, 0),
      IsCA:                  true,
      ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
      KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
      BasicConstraintsValid: true,
   }


   privateKey, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) // ecdsa.PrivateKey
   publicKey := &privateKey.PublicKey // ecdsa.PublicKey

   cert_b, err := x509.CreateCertificate(rand.Reader, ca, ca, publicKey, privateKey)

   if err != nil {
      log.Println("create ca failed", err)
      return
   }

   // Public key
   certOut, err := os.Create("bob.crt")
   pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert_b})
   certOut.Close()
   log.Print("written cert.pem\n")

   // Private key
   keyOut, err := os.OpenFile("bob.key", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
   x509Encoded, _ := x509.MarshalECPrivateKey(privateKey)
   pem.Encode(keyOut, &pem.Block{Type: "ECDSA PRIVATE KEY", Bytes: x509Encoded})
   keyOut.Close()
   log.Print("written key.pem\n")


   // get pubic key from certificate
   cert, _ = x509.ParseCertificate(cert_b)
   ecdsaPublicKey := cert.PublicKey.(*ecdsa.PublicKey)
   log.Print(ecdsaPublicKey)

   // get pubic key from certificate pem file
   certFile, err := os.Open("bob.crt")
   if err != nil {
      fmt.Println(err)
      os.Exit(1)
   }

   pemfileinfo, _ := certFile.Stat()
   size := pemfileinfo.Size()
   pembytes := make([]byte, size)
   buffer := bufio.NewReader(certFile)
   _, err = buffer.Read(pembytes)
   data, _ := pem.Decode([]byte(pembytes))
   certFile.Close()


   cert2, _ := x509.ParseCertificate(data.Bytes)
   ecdsaPublicKey2 := cert2.PublicKey.(*ecdsa.PublicKey)
   log.Print(ecdsaPublicKey2)

   // certificate validation
   checkSign:= cert2.CheckSignatureFrom(ca)
   if checkSign == nil {
      log.Print("check ok")
   }

}

위 예제들의 완전한 코드는 이런 예제를 이용하여 하이퍼레저 패브릭 MSP를 시뮬레이션 해보는 작업과 함께 다음 글에서 MSP 코드와 함께 Github에 올릴 예정입니다. 


다음 글: 400라인의 go코드로 구현한 하이퍼레저 패브릭 [4-1]-  MSP (2) (작성중..) 




레퍼런스:

https://fale.io/blog/2017/06/05/create-a-pki-in-golang/ 
https://8gwifi.org/docs/go-rsa.jsp 
https://github.com/hyperledger/fabric/blob/release-1.4/common/tools/cryptogen/main.go 
https://github.com/ethereum/go-ethereum/tree/master/p2p 
https://github.com/hyperledger/fabric/tree/release-1.4/msp 



Comments