이번 포스팅에서는 블록체인 네트워크 상에서 중요한 개념 중에 하나인 개인키(Private Key) , 공개키(Public Key) , 서명(Signature)에 대해 알아보고 실제 어떤식으로 개인키, 공개키, 서명, 지갑이 만들어지는 코드로써 구현해보고자 한다.
< 목차 >
- 개인키, 공개키, 서명
- 코드로 구현해보기
- 지갑 주소 / 계정 만들기
1. 개인키, 공개키, 서명
개인키, 공개키 그리고 서명을 이용한 신원 인증 방식에 대해 알기 위해서는 우선 "분산원장"에 대한 이해가 필요하다. 현존하는 금융 시스템은 은행이라는 금융 기관이 하나의 장부를 가지고 있고 해당 장부에 모든 거래 내역들이 기록되는 방식이다. 은행이라는 금융 기관이 가지고 있는 하나의 장부에 모든 거래들이 의존될 수 밖에 없는 중앙 집권화된 방식이라고 할 수 있다. 분산원장은 이와는 반대되는 개념으로 거래에 참여하는 모든 참여자들이 장부를 가지고 있고 거래가 발생했을 때 해당 거래 내역을 각자의 장부 안에 기록하는 방식이다. 하나의 장부에 의존하는 방식이 아닌 참여자 모두가 장부를 갖고 있는 것이기 때문에 탈중앙화된 거래 시스템이라 할 수 있다. 이러한 분산원장 방식은 거래 내역의 위변조가 어렵다는 점에서 중앙 집권화된 거래 방식에 비해 보안성이 우수하다.
분산원장 기술에서 중요한 것은 바로 신원을 인증하는 방식이다. 그리고 이 신원 인증 방식에서 사용하는 것이 개인키, 공개키, 서명이다. 암호화 방식에는 대칭형 암호화 방식과 비대칭형 암호화 방식이 있다. 대칭형 암호화 방식은 하나의 키를 사용해 암호화와 복호화가 모두 가능한 방식이다. 반면 비대칭형 암호화 방식에서는 개인키와 공개키라는 1:1로 매칭되는 키들이 존재하고 개인키로 암호화된 것은 반드시 공개키를 사용해서만 복호화가 가능하다.
비대칭 암호화 방식에서의 핵심은 공개키는 반드시 개인키를 통해서만 만들어질 수 있다는 사실이다. 그리고 공개키를 사용해 개인키를 찾아내는 것은 불가능하다.
비대칭 암호화 방식을 이용한 신원인증은 다음과 같이 이뤄진다.
- A라는 사람이 개인키를 사용해 특정 텍스트를 암호화한다.
- 공개키와 암호를 B에게 전달한다.
- B는 공개키를 사용해 암호를 복호화 할 수 있고 텍스트가 A에 의해 작성되었다는 것을 알게 된다.
공개키는 반드시 개인키를 통해서만 생성될 수 있기 때문에 A의 공개키로 특정 암호가 복호화된다는 것은 해당 암호가 A에 의해 암호화 되었다는 사실을 증명해주게 된다.
개인키와 공개키 사이의 관계에 대해 이해했다면, 다음으로 알아보아야 할 것은 디지털 서명(signature)이다. 서명은 앞서 언급한 개인키를 사용해서 만들어진다. 서명이 이루어지는 과정은 다음과 같다.
- 암호화 하고 싶은 데이터를 SHA256 방식으로 해싱(hashing)한다.
- 개인키를 사용해 해시(hash)값으로 서명(signature)을 만든다.
- 서명과 함께 공개키를 제 3자에게 건내준다.
- 제 3자는 공개키를 이용해 서명(signature)을 복호화 할 수 있다.
- 복호화된 해시(hash)값과 데이터를 해싱(hashing) 해서 나온 해시(hash)값을 비교한다.
이러한 디지털 서명을 통해 우리가 알 수 있는 것은 두가지이다. B가 갖고 있는 공개키로 복호화 할 수 있는 것은 오직 A의 개인키로 암호화된 내용들 뿐이기 때문에 B의 입장에서는 해당 서명(signature)이 A에 의해서 만들어진 것임을 확신할 수 있다. 그리고 데이터를 해싱해서 얻은 해시값과 서명을 복호화해서 얻은 해시값의 일치 여부를 검사해 데이터의 위변조 여부 또한 파악할 수 있다.
이제 코드를 보면서 실제 개인키, 공개키, 서명, 그리고 검증의 내용들이 어떤식으로 이루어지는지 살펴보자.
2. 코드로 구현해보기
Jest를 사용해 테스트 코드를 작성해보면서 다음의 단계들을 밟아나갈 예정이다.
- 개인키 생성하기
- 개인키를 사용해 공개키 만들기
- 개인키를 사용해 서명 만들기
- 검증하기
(1) 개인키 생성하기
실제 블록체인 네트워크 상에서 개인키를 생성하는 방식은 간단하다. 256 자리의 2진수로 이루어진 랜덤 값을 64자리의 16진수 값으로 만든 것이 바로 개인키가 된다.
개인키를 생성하는 테스트 코드는 아래와 같다.
// 개인키 생성 테스트 코드
import { randomBytes } from "crypto";
describe("지갑 이해하기", () => {
let privKey: string;
it("개인키 생성하기", () => {
privKey = randomBytes(32).toString("hex");
console.log("개인키 : ", privKey);
console.log("길이 : ", privKey.length);
});
});
256자리의 2진수 랜덤값을 만들어주기 위해 "crypto" 모듈 안에 있는 randomBytes( ) 함수를 사용하였다. randomBytes(32)에 의해 256 bit의 랜덤한 값이 만들어지고 toString('hex')를 통해 16진수로 나타내주었다.
(32 bytes = 256 bit , 1 byte = 8 bit)
(2) 개인키를 사용해 공개키 만들기
개인키와 페어를 이루는 공개키를 만들기 위해서는 라이브러리의 도움을 받아야한다. 실제 개인키를 사용해 공개키를 생성하는 알고리즘에는 타원곡선함수라는 수학적인 개념이 내포되어 있다. 너무나도 수학적인 내용이므로 관심있는 분들은 아래의 글을 참고하기 바란다.
https://brunch.co.kr/@nujabes403/13
타원곡선 알고리즘을 직접 구현하는 것은 너무나도 먼 얘기이므로 개인키를 사용해 공개키를 쉽게 만들 수 있도록 함수를 제공해주는 라이브러리를 하나 소개하고자 한다. "elliptic"이라는 라이브러리로 해당 라이브러리를 사용하면 인스턴스의 메소드 함수를 통해 공개키와 서명을 생성하고 검증하는 것이 가능하다.
npm install elliptic
npm i --save-dev @types/elliptic
공개키 생성 코드는 다음과 같다.
// 공개키 생성 테스트 코드
import { randomBytes } from "crypto";
import elliptic from "elliptic";
// elliptic 인스턴스 생성
const ec = new elliptic.ec("secp256k1");
describe("지갑 이해하기", () => {
let privKey: string;
let pubKey: string;
it("개인키 생성하기", () => {
privKey = randomBytes(32).toString("hex");
console.log("개인키 : ", privKey);
console.log("길이 : ", privKey.length);
});
it("공개키 생성하기", () => {
const keyPair = ec.keyFromPrivate(privKey);
pubKey = keyPair.getPublic().encode("hex", true);
console.log("공개키 : ", pubKey);
console.log("길이 : ", pubKey.length);
});
});
공개키를 만들어주는 함수는 elliptic 인스턴스의 메소드 함수이기 때문에 const ec = new elliptic.ec("secp256k1") 를 통해 ec 라는 인스턴스를 만들어주었다. 현재 우리가 생성한 개인키는 string 타입의 64자리 16진수 값이다. 따라서 ec.keyFromPrivate( privKey ) 메소드를 사용해 컴퓨터가 읽을 수 있는 값으로 변환하는 작업을 거쳐준다.
변환된 keyPair를 가지고 keyPair.getPublic( ).encod("hex", true) 메소드를 사용해 공개키를 만들어주었다.
여기서 공개키의 길이가 64가 아닌 66으로 나오는 이유는 공개키가 만들어지는 타원곡선 알고리즘 상에서 y값이 짝수일 경우 앞자리에 02를, y값이 홀수일 경우 앞자리에 03을 붙여주기 때문이다.
(3) 개인키를 사용해 서명 만들기
개인키를 사용해 서명을 만드는 과정 역시 elliptic 라이브러리에서 제공해주는 함수를 이용해 쉽게 처리가 가능하다.
// 서명 만들기 테스트 코드
import { randomBytes } from "crypto";
import elliptic from "elliptic";
import { SHA256 } from "crypto-js";
// elliptic 인스턴스 생성
const ec = new elliptic.ec("secp256k1");
describe("지갑 이해하기", () => {
let privKey: string;
let pubKey: string;
let signature: elliptic.ec.Signature;
it("개인키 생성하기", () => {
privKey = randomBytes(32).toString("hex");
console.log("개인키 : ", privKey);
console.log("길이 : ", privKey.length);
});
it("공개키 생성하기", () => {
const keyPair = ec.keyFromPrivate(privKey);
pubKey = keyPair.getPublic().encode("hex", true);
console.log("공개키 : ", pubKey);
});
it("서명 만들기", () => {
// 필요한 값 : 개인키 , hash값 (transaction hash, 거래내역을 해싱한 값)
const keyPair = ec.keyFromPrivate(privKey);
const hash = SHA256("transaction data").toString();
signature = keyPair.sign(hash, "hex");
console.log("서명 : ", signature);
});
});
서명(signature)을 만들기 위해 필요한 값은 개인키(privKey)와 해시(transaction hash)값이다.
- 개인키 (privKey)
- 해시값 (transaction hash)
여기서 얘기하는 해시값은 실제 거래내역이 담겨있는 데이터를 SHA256 방식으로 해싱(hashing)한 값을 일컫는다.
keyPair.sign( hash, "hex" ) 메소드를 통해 트랜잭션 내용을 해싱(hashing)한 해시값을 개인키로 암호화하고 16진수로 변환해준다. 다음과 같이 서명(signature)이 만들어진 것을 확인할 수 있다.
(4) 검증하기
마지막으로 검증하는 테스트 코드를 작성해보자. 검증하는 과정에서 필요한 값은 다음과 같다.
- 해시값 (transaction hash)
- 서명 (signature)
- 공개키 (pubKey)
공개키를 사용해 서명을 복호화했을 때 나온 값이 해시(transaction hash)값과 일치한다면 해당 서명은 공개키를 생성한 개인키 소유자에 의해 만들어진 서명임이 입증된다. 마찬가지로 elliptic에서 제공해주는 메소드를 사용하면 검증하는 코드를 다음과 같이 쉽게 작성할 수 있다.
// 검증 테스트 코드
import { randomBytes } from "crypto";
import elliptic from "elliptic";
import { SHA256 } from "crypto-js";
// elliptic 인스턴스 생성
const ec = new elliptic.ec("secp256k1");
describe("지갑 이해하기", () => {
let privKey: string;
let pubKey: string;
let signature: elliptic.ec.Signature;
it("개인키 생성하기", () => {
privKey = randomBytes(32).toString("hex");
console.log("개인키 : ", privKey);
console.log("길이 : ", privKey.length);
});
it("공개키 생성하기", () => {
const keyPair = ec.keyFromPrivate(privKey);
pubKey = keyPair.getPublic().encode("hex", true);
console.log("공개키 : ", pubKey);
});
it("서명 만들기", () => {
// 필요한 값 : 개인키 , hash값 (transaction hash, 거래내역을 해싱한 값)
const keyPair = ec.keyFromPrivate(privKey);
const hash = SHA256("transaction data").toString();
signature = keyPair.sign(hash, "hex");
console.log("서명 : ", signature);
});
it("검증하기 (verify)", () => {
// 필요한 값 : 서명, 공개키, hash값
const hash = SHA256("transaction data").toString();
const verify = ec.verify(hash, signature, ec.keyFromPublic(pubKey, "hex"));
console.log(verify);
});
});
ec.verify( ) 메소드의 인자값으로 검증에 필요한 해시값(hash), 서명(signature), 공개키(ec.keyFromPublic(pubKey, "hex")) 를 전달해주면 trure/false의 boolean 값을 return 받을 수 있다.
"transaction data" 라는 동일한 내용을 해시값(transaction hash)으로 만들어 검증에 사용했기 때문에 검증에 성공하게 되었고 위와같이 true 값이 반환된 것을 확인할 수 있다.
3. 지갑 주소 / 계정 만들기
코인 거래를 해본 사람들은 지갑 프로그램에 대해 이미 익숙히 알고있을 것이다. "지갑 주소"란 쉽게 말해 계좌번호와 같은 개념이다. 지갑 주소는 공개키를 이용해 생성하게 되는데 코인별로 생성하는 방식이 상이하다. 예를 들어 비트코인의 경우 공개키를 사용해 2번의 암호화 과정을 거쳐 주소를 생성하게 된다. 반면 이더리움의 경우 64자리(32 bytes)의 공개키에서 앞의 24자리(12 bytes)를 잘라낸 40자리의 값을 계정으로 사용한다.
메타마스크, 카이카스 등과 같이 우리가 사용하는 지갑 프로그램들을 통해 지갑 주소를 생성할 경우 프로그램 내부적으로 개인키를 생성하여 공개키를 만들고 다시 공개키를 사용해 지갑 주소를 만들게 된다. 그리고 그렇게 만들어진 지갑 주소를 우리에게 보여주는 것이다.
지갑 프로그램 내에서 특정 지갑 주소를 입력해 코인을 전송하는 메커니즘은 지갑 프로그램이 개인키를 사용해 트랜잭션 내용을 토대로 서명을 만들고 공개키와 함께 블록체인 네트워크 상에 전송해주는 방식으로 작동하는 것이다. 결국 지갑 프로그램에 있어서는 개인키를 어떻게 보관하는가가 가장 중요한 이슈가 된다. 따라서 개인키 보관 방식은 지갑 프로그램 별로 상이하며 대표적인 지갑 프로그램인 메타마스크의 경우 사용자의 로컬 디렉토리 내에 개인키를 보관한다고 한다.
마지막으로 이더리움 방식으로 계정을 만들어 보고자 한다. (비트코인은 너무 복잡,,)
// 계정 만들기 테스트 코드
import { randomBytes } from "crypto";
import elliptic from "elliptic";
import { SHA256 } from "crypto-js";
// elliptic 인스턴스 생성
const ec = new elliptic.ec("secp256k1");
describe("지갑 이해하기", () => {
let privKey: string;
let pubKey: string;
let signature: elliptic.ec.Signature;
it("개인키 생성하기", () => {
privKey = randomBytes(32).toString("hex");
console.log("개인키 : ", privKey);
console.log("길이 : ", privKey.length);
});
it("공개키 생성하기", () => {
const keyPair = ec.keyFromPrivate(privKey);
pubKey = keyPair.getPublic().encode("hex", true);
console.log("공개키 : ", pubKey);
console.log("길이 : ", pubKey.length);
});
it("서명 만들기", () => {
// 필요한 값 : 개인키 , hash값 (transaction hash, 거래내역을 해싱한 값)
const keyPair = ec.keyFromPrivate(privKey);
const hash = SHA256("transaction data").toString();
signature = keyPair.sign(hash, "hex");
console.log("서명 : ", signature);
});
it("검증하기 (verify)", () => {
// 필요한 값 : 서명, 공개키, hash값
const hash = SHA256("transaction data").toString();
const verify = ec.verify(hash, signature, ec.keyFromPublic(pubKey, "hex"));
console.log(verify);
});
it("계정 만들기(지갑 주소)", () => {
const buffer = Buffer.from(pubKey);
const address = buffer.slice(26).toString();
console.log("계정 : ", address);
});
});
이더리움 방식으로 계정을 만드는 것은 간단하다. 만들어 놓은 공개키(pubKey)에서 앞의 24자리를 잘라내고 40자리만을 남겨주면 된다. buffer.slice(26).toString( ) 에서 slice( ) 인자값으로 26을 넣은 이유는 elliptic을 사용해 만들어진 공개키의 경우 앞자리에 02 혹은 03이 붙기 때문에 이 값도 제거해주기 위함이다.
'BlockChain' 카테고리의 다른 글
BlockChain - [블록체인 네트워크] 트랜잭션 (0) | 2022.06.21 |
---|---|
BlockChain - 블록체인 지갑 서버 만들기 (1) (0) | 2022.06.20 |
BlockChain - 블록체인 P2P 네트워크 만들기 (3) (2) | 2022.06.16 |
BlockChain - 블록체인 P2P 네트워크 만들기 (2) (0) | 2022.06.15 |
BlockChain - 블록체인 P2P 네트워크 만들기 (1) (1) | 2022.06.14 |