체인의정석

Cloud HSM 구현 + go 환경에서의 트랜잭션 서명 본문

개발/docker & linux

Cloud HSM 구현 + go 환경에서의 트랜잭션 서명

체인의정석 2025. 7. 28. 19:07
728x90

상황 : 클라우드 HSM을 통해서 트랜잭션을 쏘는 소스를 인계받는상황에서 찾아본 내용 정리

AWS Cloud HSM 작업 순서

먼저 cloud hsm의 경우 IAM 사용자가 생성되어 있어야 하고, VPC와 클러스터를 생성해야 한다.

VPC 안에만 클라우드 hsm을 설치할 수 있기 때문에 VPC를 먼저 생성한 후에

아래 가이드를 보고 클러스터를 생성한다.

https://docs.aws.amazon.com/cloudhsm/latest/userguide/create-cluster.html

이후 EC2 인스턴스를 생성해야 하는데 이때 Amazon Machine Image(AMI)를 선택한다. 

HSM의 경우 접근 권한이 매우 중요하기 때문에 보안그룹을 수정한 후 해당 보안 그룹을 ec2에 할당하고, 만든 ec2를 cloudHSM과 연결해야 한다.

https://docs.aws.amazon.com/cloudhsm/latest/userguide/configure-sg-client-instance.html

 

Configure the Client Amazon EC2 instance security groups for AWS CloudHSM - AWS CloudHSM

You can assign a maximum of five security groups to an Amazon EC2 instance. If you have reached the maximum limit, you must modify the default security group of the Amazon EC2 instance and the cluster security group: In the default security group, do the f

docs.aws.amazon.com

Cloud HSM이 구현되고 난 후에는 다음과 같은 사용이 가능하다.

1. CloudHSM CLI를 사용하여 HSM 암호화 사용자 생성하는 법

https://docs.aws.amazon.com/cloudhsm/latest/userguide/create-user-cloudhsm-cli.html

먼저 유저를 만들어 준다. 내가 사용하는 환경에서는 각 환경별로 다른 role을 부여해서 하나의 Hsm을 관리하였다. (HSM은 운영비가 비싸기 때문에 하나로 모두 사용하지만 유저는 나누어서 관리)

/opt/cloudhsm/bin/cloudhsm-cli interactive //CloudHSM CLI  모드
login --username <username> --role admin // 로그인 하는 법

Enter password: //패스워드 설정
{
  "error_code": 0,
  "data": {
    "username": "<USERNAME>",
    "role": "admin"
  }
}

user create --username <username> --role crypto-user //유저생성

2. AWS의 CloudHSM 클러스터에서 타원형곡선 알고리즘기반 키 페어 만드는 법

https://docs.aws.amazon.com/cloudhsm/latest/userguide/cloudhsm_cli-key-generate-asymmetric-pair-ec.html

 

Generate an asymmetric EC key pair with CloudHSM CLI - AWS CloudHSM

When generating a key with quorum controls, the key must be associated with a minimum number of users equal to the largest key quorum value. Associated users include the key owner and Crypto Users with whom the key is shared with. To determine the number o

docs.aws.amazon.com

 

aws-cloudhsm > help key generate-asymmetric-pair ec
Generate an Elliptic-Curve Cryptography (ECC) key pair

Usage: key generate-asymmetric-pair ec [OPTIONS] --public-label <PUBLIC_LABEL> --private-label <PRIVATE_LABEL> --curve <CURVE>

Options:
      --cluster-id <CLUSTER_ID>
          Unique Id to choose which of the clusters in the config file to run the operation against. If not provided, will fall back to the value provided when interactive mode was started, or error
      --public-label <PUBLIC_LABEL>
          Label for the public key
      --private-label <PRIVATE_LABEL>
          Label for the private key
      --session
          Creates a session key pair that exists only in the current session. The key cannot be recovered after the session ends
      --curve <CURVE>
          Elliptic curve used to generate the key pair [possible values: prime256v1, secp256r1, secp224r1, secp384r1, secp256k1, secp521r1]
      --public-attributes [<PUBLIC_KEY_ATTRIBUTES>...]
          Space separated list of key attributes to set for the generated EC public key in the form of KEY_ATTRIBUTE_NAME=KEY_ATTRIBUTE_VALUE
      --private-attributes [<PRIVATE_KEY_ATTRIBUTES>...]
          Space separated list of key attributes to set for the generated EC private key in the form of KEY_ATTRIBUTE_NAME=KEY_ATTRIBUTE_VALUE
      --share-crypto-users [<SHARE_CRYPTO_USERS>...]
          Space separated list of Crypto User usernames to share the EC private key with
      --manage-private-key-quorum-value <MANAGE_PRIVATE_KEY_QUORUM_VALUE>
          The quorum value for key management operations for the private key
      --use-private-key-quorum-value <USE_PRIVATE_KEY_QUORUM_VALUE>
          The quorum value for key usage operations for the private key
  -h, --help
          Print help

예시

key generate-asymmetric-pair ec \
    --curve secp224r1 \
    --public-label ec-public-key-example \
    --private-label ec-private-key-example

위와 같이 퍼블릭키에 대한 라벨과 프라이빗 키에 대한 라벨을 각각 지정해 준 상태로 키를 생성해 준다.

만약 서명을 하고 싶을 경우에는 타원형 곡선방식의 키에서는 디폴트 값이 false이기 때문에 set-attribute에서 싸인 가능하게 변경하는 부분이 필요하다.

https://docs.aws.amazon.com/cloudhsm/latest/userguide/cloudhsm_cli-key-attributes-table.html

 

Supported attributes for CloudHSM CLI - AWS CloudHSM

Supported attributes for CloudHSM CLI As a best practice, only set values for attributes you wish to make restrictive. If you don’t specify a value, CloudHSM CLI uses the default value specified in the table below. The following table lists the key attri

docs.aws.amazon.com

key set-attribute --filter attr.label="프라이빗키 label" --name sign --value true

따라서 위에 명령어 처럼 이미 만들어 둔 label에다가 서명이 가능하게 된다.

내가 인계받은 케이스에서는 유저별로 1개의 키 페어를 만들고 각각 서명 가능하게 바꾸는 식으로 설정한 후에 cloud HSM을 사용하였다.

 

3. Go 모듈에서 hsm에 서명하기

https://github.com/miekg/pkcs11

 

GitHub - miekg/pkcs11: pkcs11 wrapper for Go

pkcs11 wrapper for Go. Contribute to miekg/pkcs11 development by creating an account on GitHub.

github.com

 go에서는 해당 라이브러리를 사용하여 서명이 가능하다.

p := pkcs11.New("/usr/lib/softhsm/libsofthsm2.so")
err := p.Initialize()
if err != nil {
    panic(err)
}

readme에 보면 예시가 있는데 위의 코드처럼 라이브러리 경로를 넣어준 후 초기화를 먼저 해준다.

이후 HSM 세션을 열기 위해 슬롯 목록을 가져온다. (등록한 cloud HSM의 유저 슬롯들을 가져오는것)

slots, err := p.GetSlotList(true)
if err != nil {
    panic(err)
}

이후 서명이 가능한 세션을 열어준다.

session, err := p.OpenSession(slots[0], pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION)
if err != nil {
    panic(err)
}
defer p.CloseSession(session)

*defer는 함수가 모두 실행되고 난 뒤에 작동하기 때문에 세션 종료를 바로 설정해 준다.

이후에는 모든 활성화 된 서명 작업을 종료한다.

	_, err := p.SignFinal(session)
	if err != nil && !isOperationNotInitialized(err) {
		log.Printf("Failed to finalize sign operation: %v", err)
	}

이후 p와 session 변수를 받아서 서명작업이 종료되었음을 확인하고 만약 서명 부분이 시작도 안된 상황이 아니라면 오류를 리턴시켜 준다.

err = p.Login(session, pkcs11.CKU_USER, "1234")
if err != nil {
    panic(err)
}
defer p.Logout(session)

세션이 활성화 되면 로그인을 해준 후 defer로 작업이 끝났을 때 세션에서 로그아웃 또한 미리 설정해 준다. 로그인 이후에는 cloudHSM내부의 키 중에서 우리가 서명하고 싶은 딱 그 키만 찾아야 한다. 검색이 가능해야되는 것.

이후 https://github.com/miekg/pkcs11/blob/master/pkcs11_test.go

 

pkcs11/pkcs11_test.go at master · miekg/pkcs11

pkcs11 wrapper for Go. Contribute to miekg/pkcs11 development by creating an account on GitHub.

github.com

해당 예제를 참고해 아래 예제 처럼 원하는 키 핸들을 찾을 수 있다.

// 검색 조건 (예: "라벨이 myKey 이고 EC 키인 것")
template := []*pkcs11.Attribute{
    pkcs11.NewAttribute(pkcs11.CKA_LABEL, "myKey"),
    pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_EC),
}

// 1단계: 검색 초기화
err := p.FindObjectsInit(session, template)
if err != nil {
    return 0, err
}

// 2단계: 검색 실행 (최대 1개만 요청)
handles, _, err := p.FindObjects(session, 1)
if err != nil {
    return 0, err
}

// 3단계: 검색 종료
err = p.FindObjectsFinal(session)
if err != nil {
    return 0, err
}

이렇게 3단계에 걸친 . FindObjectsInit → FindObjects → FindObjectsFinal의 3단계는 PKCS#11 표준에 해당된다.

단계함수역할

1️⃣ FindObjectsInit() 검색 조건(Attribute template)을 설정하고, 검색 세션을 초기화
2️⃣ FindObjects() 지정한 조건에 맞는 객체들을 순차적으로 가져옴 (1개씩 또는 N개씩)
3️⃣ FindObjectsFinal() 검색을 종료하고 HSM 내부 검색 상태를 정리

1. 검색 상태를 세션에 유지해야 하기 때문

  • FindObjectsInit() 호출 이후, HSM은 내부적으로 "검색 포인터"를 유지합니다.
  • FindObjects()를 반복 호출하면 그 포인터를 기반으로 다음 객체들을 점진적으로 리턴합니다.
  • 이 방식은 수백, 수천 개의 키가 존재할 수 있는 경우에 효율적입니다.

2. 리소스 누수 방지를 위해 명시적 종료 필요

  • FindObjectsFinal()을 호출하지 않으면 세션 상태가 계속 유지되어, HSM 리소스를 낭비하거나 잠글 수 있음.

 

위 예제를 통해서 개인키와 공개키 핸들을 가져올 수 있다. 여기서 template의 경우 필터 역할도 해주기 때문에 아래와 같이 서명 가능한 개인키 중 특정 라벨만 찾고 싶다면 secp256k1의 식별자를 넣어주면 된다.

secp256k1 곡선의 OID (1.3.132.0.10)를 ASN.1 DER로 인코딩한 값고정된 값입니다. 즉:

항상 0x06 0x05 2B 81 04 00 0A 입니다.

이 값은 암호 표준에 따라 정해진 불변의 규칙으로 인코딩된 것이며, 다음과 같은 이유로 절대 바뀌지 않습니다:


✅ 왜 고정인가?

1. OID는 전 세계적으로 유일한 식별자

  • 1.3.132.0.10은 국제 표준에서 secp256k1을 의미하는 고정된 OID입니다.
  • 이는 SEC2 표준 문서에도 명시돼 있습니다.
  • 표준화 기구(ISO, ANSI, SECG 등)에서 발급하는 영구 식별 번호입니다.

2. ASN.1 DER 인코딩은 결정적 인코딩 방식

  • DER(Distinguished Encoding Rules)은 같은 OID에 대해 항상 동일한 이진 결과를 생성합니다.
  • 즉, 1.3.132.0.10 → DER 인코딩 → 항상 06 05 2B 81 04 00 0A

3. HSM, PKI, 인증서, 스마트카드 등에서 모두 이 값을 사용

  • HSM에서 CKA_EC_PARAMS에 이 값을 넣어야만 secp256k1 키로 인식함
  • 인증서 내에서 ECPublicKey의 parameters 필드에도 정확히 이 값이 들어갑니다
  • Ethereum, Bitcoin, zkRollup 등 모든 secp256k1 기반 시스템이 이 DER 값을 기본으로 사용
	// ✅ secp256k1 OID = 1.3.132.0.10 의 ASN.1 DER 인코딩 값
	oidSecp256k1 := []byte{0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x0A}

	// 🔍 개인 키 핸들 찾기
	template := []*pkcs11.Attribute{
		pkcs11.NewAttribute(pkcs11.CKA_LABEL, "MySecp256k1Key"),
		pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_EC),
		pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, oidSecp256k1),
	}

따라서 위와 같이 템플릿을 정해주면 개인키 서명만 따로 뽑아낼 수 있으며,

이렇게 뽑아낸 개인키를 SignInit 함수에 넣어주면 된다. (privHandler 부분)

	// 6. ECDSA 서명 초기화
	mech := pkcs11.NewMechanism(pkcs11.CKM_ECDSA, nil)
	if err := p.SignInit(session, []*pkcs11.Mechanism{mech}, privHandle); err != nil {
		log.Fatalf("SignInit failed: %v", err)
	}

	// 7. 서명 실행
	signature, err := p.Sign(session, txHash)
	if err != nil {
		log.Fatalf("Sign failed: %v", err)
	}

위 예시를 통해 p.SignInit과 P.sign을 통해서 서명 값을 받아온다.

이때 사용하는 서명은 CKM_ECDSA 이기 때문에 SignInit 시에 해당 알고리즘을 사용할 것이라고 명시해 두고 이후에 Sign을 한다.

https://docs.aws.amazon.com/cloudhsm/latest/userguide/pkcs11-mechanisms.html#pkcs11-mech-function-signverify

 

Supported mechanisms for the PKCS #11 library for AWS CloudHSM Client SDK 5 - AWS CloudHSM

[1] When performing AES-GCM encryption, the HSM does not accept initialization vector (IV) data from the application. You must use an IV that it generates. The 12-byte IV provided by the HSM is written into the memory reference pointed to by the pIV elemen

docs.aws.amazon.com

여기서 txhash는 다음과 같이 사용된다.

// 1. 먼저 트랜잭션 데이터를 RLP 인코딩하고
rlpTx := rlp.EncodeToBytes(tx) // (pseudo code)

// 2. 이더리움에서는 Keccak256 해시를 사용
txHash := crypto.Keccak256(rlpTx)

// 3. 이 값을 Sign에 넣음
sig, err := p.Sign(session, txHash)

 

Sign 에는 왜 txHash를 넣는가?

🔐 PKCS#11의 CKM_ECDSA 메커니즘

  • 이 메커니즘(CKM_ECDSA)은 해싱을 포함하지 않습니다.
  • 즉, "바이트 그대로" 서명하는 메커니즘입니다.
  • 따라서, Ethereum이나 Bitcoin과 같은 시스템에서는 트랜잭션 전체를 미리 Keccak256 또는 SHA256 해싱해서
    → 그 해시값(txHash) 에 서명을 합니다.

✅ 실제 ECDSA 서명 처리 방식

  1. 트랜잭션 또는 메시지 전체 → Keccak256 해싱 → 32바이트 txHash
  2. 이 txHash를 HSM에 전달
  3. CKM_ECDSA는 이 txHash를 입력으로 받아 → (R, S) 서명값을 생성

🔍 코드에서 해당되는 부분

signatureRS, err := p.Sign(session, txHash)

여기서 txHash는 미리 해싱된 32바이트 메시지여야 하며,
ECDSA 서명은 이 바이트열 위에서 R/S 값을 생성합니다.


✅ Ethereum에서 서명할 때도 마찬가지

예를 들어, Ethereum 트랜잭션 서명은 다음과 같은 과정을 따릅니다:

// 트랜잭션 구조 → RLP 인코딩 → Keccak256 해시 txHash := crypto.Keccak256(tx.RlpEncoded()) // 서명 입력은 해시값 signature := Sign(txHash, privateKey)

→ 여기서 txHash가 정확히 지금 코드의 txHash에 해당합니다.


만약 CKM_ECDSA_SHA256을 쓴다면?

그때는 해시를 직접 하지 않아도 됩니다:

p.SignInit(session, []*Mechanism{NewMechanism(CKM_ECDSA_SHA256, nil)}, privHandle) signature, err := p.Sign(session, rawMessage) // raw 메시지를 입력함

하지만 Ethereum에서는 반드시 Keccak256을 직접 적용해야 하기 때문에
→ CKM_ECDSA를 써야 하고
→ 그래서 이미 해싱된 txHash를 Sign()에 넣는 것이 맞습니다.


✅ 요약

항목설명
txHash 이미 Keccak256 또는 SHA256 등으로 해싱된 32바이트 메시지
CKM_ECDSA 해싱을 포함하지 않음 – "해시를 직접 넣어야 함"
p.Sign(session, txHash) HSM이 txHash에 대해 ECDSA 서명을 수행

*  예시 (GPT 생성된것으로 문법 및 순서만 참고)

package main

import (
	"crypto/ecdsa"
	"encoding/hex"
	"fmt"
	"log"
	"math/big"
	"os"

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/rlp"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/miekg/pkcs11"
)

func main() {
	// ✅ 트랜잭션 파라미터 정의
	nonce := uint64(0)
	to := common.HexToAddress("0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520")
	value := big.NewInt(1000000000000000000) // 1 ETH
	gasLimit := uint64(21000)
	gasPrice := big.NewInt(20000000000) // 20 Gwei
	chainID := big.NewInt(1)            // mainnet

	// EIP-155 트랜잭션 생성 (서명 전용)
	tx := types.NewTransaction(nonce, to, value, gasLimit, gasPrice, nil)
	signer := types.NewEIP155Signer(chainID)
	txHash := signer.Hash(tx)

	fmt.Printf("🚀 트랜잭션 해시: %x\n", txHash)

	// ✅ HSM 초기화
	libPath := os.Getenv("SOFTHSM_LIB")
	if libPath == "" {
		log.Fatal("SOFTHSM_LIB 환경변수를 설정하세요")
	}
	p := pkcs11.New(libPath)
	if err := p.Initialize(); err != nil {
		log.Fatalf("HSM 초기화 실패: %v", err)
	}
	defer p.Destroy()
	defer p.Finalize()

	slots, _ := p.GetSlotList(true)
	session, _ := p.OpenSession(slots[0], pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION)
	defer p.CloseSession(session)
	_ = p.Login(session, pkcs11.CKU_USER, "1234")
	defer p.Logout(session)

	// ✅ 키 핸들 찾기
	oidSecp256k1 := []byte{0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x0A}
	template := []*pkcs11.Attribute{
		pkcs11.NewAttribute(pkcs11.CKA_LABEL, "MySecp256k1Key"),
		pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_EC),
		pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, oidSecp256k1),
	}
	_ = p.FindObjectsInit(session, template)
	handles, _, _ := p.FindObjects(session, 1)
	_ = p.FindObjectsFinal(session)
	privateKey := handles[0]

	// ✅ 서명 수행
	mech := pkcs11.NewMechanism(pkcs11.CKM_ECDSA, nil)
	_ = p.SignInit(session, []*pkcs11.Mechanism{mech}, privateKey)
	sig, err := p.Sign(session, txHash.Bytes())
	if err != nil {
		log.Fatalf("서명 실패: %v", err)
	}

	// ✅ R, S 분리
	r := new(big.Int).SetBytes(sig[:32])
	s := new(big.Int).SetBytes(sig[32:])

	// ✅ V 값 계산 (EIP-155)
	v := new(big.Int).Mul(chainID, big.NewInt(2))
	v.Add(v, big.NewInt(35)) // base V
	// v.Add(v, big.NewInt(1)) // recID + 27 → 일반적으로 0 or 1 붙여야 되지만 여기선 생략 (비트코인처럼 복원하지 않음)

	// ✅ 서명 포함한 최종 트랜잭션
	signedTx, err := tx.WithSignature(signer, append(sig, v.Bytes()...))
	if err != nil {
		log.Fatalf("서명 포함 트랜잭션 생성 실패: %v", err)
	}

	rawTxBytes, _ := rlp.EncodeToBytes(signedTx)
	fmt.Printf("✅ 최종 RLP 트랜잭션(hex): %s\n", hex.EncodeToString(rawTxBytes))
}
728x90
반응형
Comments