이미지 로딩 중...

타입스크립트로 비트코인 클론하기 14편 - 공개키/개인키 암호화 기초 - 슬라이드 1/9
A

AI Generated

2025. 11. 10. · 14 Views

타입스크립트로 비트코인 클론하기 14편 - 공개키/개인키 암호화 기초

블록체인의 핵심인 공개키 암호화를 타입스크립트로 직접 구현해봅니다. 타원곡선 암호화(ECDSA)의 원리부터 실제 지갑 생성, 서명/검증까지 단계별로 학습합니다.


목차

  1. 공개키_암호화란_무엇인가
  2. secp256k1_타원곡선
  3. 개인키_생성하기
  4. 공개키_도출하기
  5. 지갑_주소_생성
  6. 디지털_서명_생성
  7. 서명_검증하기
  8. 해시_함수의_역할

1. 공개키_암호화란_무엇인가

시작하며

여러분이 비트코인을 전송할 때 이런 궁금증을 가져본 적 있나요? "내 지갑의 비트코인은 어떻게 안전하게 보호되는 걸까?" 은행처럼 중앙 서버가 관리하는 것도 아닌데 말이죠.

비트코인은 공개키 암호화라는 수학적 원리로 보안을 구현합니다. 이 기술은 1970년대에 발명되어 현대 인터넷 보안의 근간이 되었고, 블록체인에서도 핵심적인 역할을 합니다.

바로 이것이 왜 비트코인이 "신뢰할 필요 없는(trustless)" 시스템이라고 불리는지에 대한 답입니다. 수학적 증명만으로 소유권을 증명하기 때문이죠.

개요

간단히 말해서, 공개키 암호화는 두 개의 키(공개키와 개인키)를 사용하는 암호화 방식입니다. 개인키는 절대 공개되어서는 안 되는 비밀번호 같은 것이고, 공개키는 누구에게나 보여줘도 되는 계좌번호 같은 것입니다.

예를 들어, 누군가 여러분에게 비트코인을 보내려면 여러분의 공개키(또는 그것에서 파생된 주소)만 알면 되지만, 그 비트코인을 사용하려면 반드시 개인키가 필요합니다. 전통적인 암호화 방식에서는 암호화와 복호화에 같은 키를 사용했다면, 공개키 암호화는 서로 다른 키를 사용합니다.

이것이 핵심입니다. 공개키 암호화의 핵심 특징은 첫째, 공개키에서 개인키를 역산하는 것이 수학적으로 거의 불가능하다는 점입니다.

둘째, 개인키로 서명한 메시지는 공개키로 검증할 수 있습니다. 셋째, 이 모든 과정이 수학적 증명에 기반하므로 중앙 기관이 필요 없습니다.

이러한 특징들이 블록체인을 탈중앙화된 시스템으로 만들어줍니다.

코드 예제

// 공개키 암호화의 기본 개념 (의사 코드)
interface KeyPair {
  privateKey: string;  // 개인키: 절대 공개하면 안 됨
  publicKey: string;   // 공개키: 누구에게나 공개 가능
}

// 개인키로 서명 생성
function sign(message: string, privateKey: string): string {
  // 개인키를 사용해 메시지에 서명
  return signature;
}

// 공개키로 서명 검증
function verify(message: string, signature: string, publicKey: string): boolean {
  // 서명이 유효한지 검증
  return isValid;
}

설명

이것이 하는 일: 공개키 암호화는 비대칭 암호화라고도 불리며, 암호화와 복호화에 서로 다른 키를 사용하는 혁신적인 방식입니다. 첫 번째로, KeyPair 인터페이스는 개인키와 공개키를 하나의 쌍으로 관리합니다.

이 두 키는 수학적으로 연결되어 있지만, 공개키로부터 개인키를 계산해내는 것은 현실적으로 불가능합니다. 이것이 바로 RSA나 ECDSA 같은 알고리즘의 보안 기반입니다.

그 다음으로, sign 함수가 실행되면 개인키를 사용해 메시지의 디지털 서명을 생성합니다. 이 과정에서 메시지는 해시 함수를 거쳐 고정된 길이의 값으로 변환되고, 이 해시값이 개인키로 암호화됩니다.

중요한 점은 같은 메시지와 같은 개인키는 항상 같은 서명을 만들어낸다는 것입니다. 마지막으로, verify 함수가 공개키를 사용해 서명의 유효성을 검증합니다.

이 함수는 서명을 공개키로 복호화하고, 그 결과가 원본 메시지의 해시값과 일치하는지 확인합니다. 일치하면 true를 반환하여, 이 서명이 정말로 해당 개인키의 소유자가 만든 것임을 증명합니다.

여러분이 이 방식을 사용하면 중앙 기관 없이도 디지털 자산의 소유권을 증명할 수 있습니다. 비트코인 네트워크의 모든 참여자가 공개키만으로 트랜잭션의 유효성을 검증할 수 있고, 이중 지불을 방지할 수 있으며, 누구도 다른 사람의 비트코인을 훔칠 수 없는 안전한 시스템이 구축됩니다.

실전 팁

💡 개인키는 절대 코드에 하드코딩하지 마세요. 환경변수나 암호화된 키 저장소를 사용해야 합니다. 깃허브에 실수로 올라간 개인키로 인해 수많은 암호화폐가 도난당한 사례가 있습니다.

💡 공개키와 지갑 주소는 다릅니다. 공개키를 해시 처리하고 인코딩해서 만든 것이 지갑 주소입니다. 보통은 주소만 공개하고 공개키는 트랜잭션 서명 시에만 노출됩니다.

💡 서명 검증은 항상 네트워크의 모든 노드가 수행합니다. 하나의 노드만 검증하는 것이 아니라 분산된 수천 개의 노드가 독립적으로 검증하기 때문에 신뢰할 수 있습니다.

💡 테스트 환경에서는 결정론적(deterministic) 키 생성을 사용하면 디버깅이 쉽습니다. 하지만 프로덕션에서는 반드시 암호학적으로 안전한 난수 생성기를 사용해야 합니다.


2. secp256k1_타원곡선

시작하며

여러분이 비트코인 코드를 보다가 "secp256k1"이라는 이상한 이름을 본 적 있나요? 도대체 이게 뭐고, 왜 비트코인은 수많은 암호화 방식 중에서 이것을 선택했을까요?

타원곡선 암호화(ECC)는 RSA 같은 전통적인 방식보다 훨씬 작은 키 크기로 같은 수준의 보안을 제공합니다. 256비트 타원곡선 키는 3072비트 RSA 키와 동등한 보안 수준을 가지는데, 이것이 모바일과 블록체인에서 중요한 이유입니다.

secp256k1은 특별히 비트코인을 위해 사타시 나카모토가 선택한 타원곡선입니다. 다른 많은 시스템이 사용하는 secp256r1보다 몇 가지 수학적 장점이 있습니다.

개요

간단히 말해서, secp256k1은 y² = x³ + 7 이라는 방정식으로 정의되는 타원곡선입니다. 이 곡선이 왜 특별한가 하면, 특정한 점(generator point)에서 시작해서 반복적으로 "더하기" 연산을 수행할 때, 결과를 예측하기는 쉽지만 역으로 계산하기는 거의 불가능하기 때문입니다.

예를 들어, G라는 점을 100번 더한 결과는 쉽게 계산할 수 있지만, 어떤 결과 점을 보고 "이건 G를 몇 번 더한 거야?"를 알아내는 것은 수학적으로 엄청나게 어렵습니다. 이것을 이산 로그 문제(Discrete Logarithm Problem)라고 합니다.

기존 RSA는 큰 소수의 곱셈을 사용했다면, ECC는 타원곡선의 기하학적 특성을 활용합니다. 더 작은 키로 더 빠른 연산이 가능합니다.

secp256k1의 핵심 특징은 첫째, 매개변수 a=0, b=7로 매우 간단한 형태라는 점입니다. 둘째, Koblitz 곡선이라 불리는 특별한 형태로 일부 최적화가 가능합니다.

셋째, 256비트 길이로 충분한 보안성을 제공하면서도 효율적입니다. 이러한 특징들이 비트코인의 성능과 보안을 동시에 만족시켜줍니다.

코드 예제

// secp256k1 곡선 파라미터 정의
const CURVE_PARAMS = {
  // 곡선 방정식: y² = x³ + 7
  a: 0n,
  b: 7n,

  // 유한체의 소수 (필드 크기)
  p: 2n ** 256n - 2n ** 32n - 977n,

  // 생성점(Generator Point) G의 좌표
  Gx: BigInt('0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798'),
  Gy: BigInt('0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8'),

  // 곡선의 위수 (생성점의 차수)
  n: BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141')
};

설명

이것이 하는 일: CURVE_PARAMS 객체는 secp256k1 타원곡선의 모든 수학적 파라미터를 정의하며, 이것이 비트코인 암호화의 기반이 됩니다. 첫 번째로, a와 b 파라미터는 타원곡선 방정식 y² = x³ + ax + b를 정의합니다.

secp256k1의 경우 a=0, b=7이므로 방정식이 y² = x³ + 7로 매우 단순해집니다. 이 단순함은 계산 최적화에 도움이 되며, 하드웨어 구현 시에도 효율적입니다.

그 다음으로, p(소수)는 유한체의 크기를 정의합니다. 실수 평면이 아닌 유한한 숫자 집합에서 타원곡선을 다루는 것인데, 이것이 컴퓨터로 정확한 계산을 가능하게 합니다.

이 p 값은 2²⁵⁶보다 약간 작은 특별한 형태의 소수로, 모듈러 연산을 최적화할 수 있습니다. Gx와 Gy는 생성점(Generator Point) G의 좌표입니다.

모든 비트코인 공개키는 이 G를 개인키만큼 곱해서 만들어집니다. 즉, 공개키 = G × 개인키 입니다.

마지막으로 n은 G의 위수로, G를 n번 더하면 무한원점(점이 없는 상태)이 됩니다. 이것이 곡선의 "크기"를 정의하며, 개인키는 1부터 n-1 사이의 숫자여야 합니다.

여러분이 이 파라미터들을 사용하면 비트코인과 완벽하게 호환되는 키 쌍을 생성할 수 있습니다. 다른 블록체인(이더리움, 비트코인 캐시 등)도 대부분 같은 secp256k1을 사용하므로, 한 번 구현하면 여러 곳에서 재사용할 수 있습니다.

또한 이 표준화된 곡선 덕분에 하드웨어 지갑, 모바일 앱 등 다양한 플랫폼 간 상호운용성이 보장됩니다.

실전 팁

💡 BigInt를 사용하는 이유는 JavaScript의 Number 타입이 2⁵³까지만 정확하게 표현할 수 있기 때문입니다. 256비트 숫자를 다루려면 BigInt가 필수입니다.

💡 실제 프로덕션 코드에서는 noble-secp256k1이나 elliptic 같은 검증된 라이브러리를 사용하세요. 암호화 알고리즘을 직접 구현하면 미묘한 버그로 인해 보안 취약점이 생길 수 있습니다.

💡 타원곡선 연산은 CPU 집약적입니다. 대량의 서명 검증이 필요하다면 배치 검증(batch verification) 기법을 사용하면 성능을 크게 개선할 수 있습니다.

💡 secp256k1의 p 값이 특별한 형태(2²⁵⁶ - 2³² - 977)인 이유는 빠른 모듈러 리덕션을 위해서입니다. 이를 "특수 소수(special prime)"라고 하며, 일반 나눗셈보다 훨씬 빠릅니다.


3. 개인키_생성하기

시작하며

여러분이 새로운 비트코인 지갑을 만들 때, 가장 먼저 일어나는 일은 무엇일까요? 바로 개인키 생성입니다.

이 하나의 숫자가 여러분의 모든 자산을 지배하게 됩니다. 개인키는 단순히 1부터 n-1 사이의 랜덤한 숫자입니다.

하지만 이 "랜덤"이라는 것이 생각보다 훨씬 중요하고 어렵습니다. 충분히 무작위적이지 않으면 공격자가 추측할 수 있고, 같은 패턴을 사용하면 여러 키가 충돌할 수도 있습니다.

실제로 2013년에는 잘못된 난수 생성기 때문에 수백 개의 비트코인 지갑이 해킹당한 사건이 있었습니다. 바로 이것이 암호학적으로 안전한 난수 생성이 왜 중요한지 보여줍니다.

개요

간단히 말해서, 개인키는 1부터 secp256k1 곡선의 위수(n) - 1 사이의 무작위 256비트 정수입니다. 이 키가 왜 안전한가 하면, 가능한 개인키의 개수가 약 2²⁵⁶개나 되기 때문입니다.

이는 우주의 모든 원자 수보다 많은 숫자입니다. 예를 들어, 세계의 모든 컴퓨터를 동원해도 여러분의 개인키를 무작위 대입으로 찾아내는 것은 수십억 년이 걸립니다.

전통적인 비밀번호 시스템에서는 서버에 저장된 해시값과 비교했다면, 블록체인에서는 개인키 자체가 유일한 증명 수단입니다. 서버도 없고 비밀번호 재설정도 없습니다.

개인키 생성의 핵심 특징은 첫째, 반드시 암호학적으로 안전한 난수 생성기(CSPRNG)를 사용해야 한다는 점입니다. 둘째, 한 번 생성된 개인키는 절대 변경할 수 없고 복구할 수도 없습니다.

셋째, 개인키를 알면 모든 것을 제어할 수 있으므로 백업이 매우 중요합니다. 이러한 특징들이 블록체인을 "내가 은행이다(Be your own bank)"라는 개념으로 만들어줍니다.

코드 예제

import crypto from 'crypto';

// secp256k1 곡선의 위수
const N = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141');

function generatePrivateKey(): string {
  let privateKey: bigint;

  do {
    // 암호학적으로 안전한 32바이트 난수 생성
    const randomBytes = crypto.randomBytes(32);
    privateKey = BigInt('0x' + randomBytes.toString('hex'));

    // 1 <= privateKey < N 범위 확인
  } while (privateKey === 0n || privateKey >= N);

  // 16진수 문자열로 반환 (64자리, 앞에 0 채움)
  return privateKey.toString(16).padStart(64, '0');
}

설명

이것이 하는 일: generatePrivateKey 함수는 비트코인 프로토콜에 완벽히 호환되는 개인키를 안전하게 생성합니다. 첫 번째로, crypto.randomBytes(32)를 사용해 운영체제의 암호학적으로 안전한 난수 생성기에서 32바이트(256비트)의 무작위 데이터를 가져옵니다.

Node.js의 crypto 모듈은 내부적으로 리눅스의 /dev/urandom이나 윈도우의 CryptGenRandom을 사용하므로, Math.random() 같은 일반 난수 생성기보다 훨씬 안전합니다. 이 무작위성의 품질이 전체 시스템의 보안을 결정합니다.

그 다음으로, 생성된 바이트를 16진수 문자열로 변환한 후 BigInt로 파싱합니다. do-while 루프는 생성된 숫자가 유효한 범위(1 이상 N 미만)에 있는지 확인합니다.

만약 0이거나 N 이상이면 다시 생성합니다. 통계적으로 이런 경우는 거의 발생하지 않지만(확률이 2⁻¹²⁸ 정도), 프로토콜 규격을 정확히 따르기 위해 필요합니다.

마지막으로, 유효한 개인키를 64자리 16진수 문자열로 반환합니다. padStart(64, '0')를 사용하는 이유는 작은 숫자일 경우 앞자리가 생략되는 것을 방지하기 위해서입니다.

예를 들어, 숫자가 작으면 50자리만 나올 수 있는데, 항상 64자리로 맞춰줘야 다른 시스템과 호환됩니다. 여러분이 이 함수를 사용하면 안전하고 표준을 준수하는 비트코인 개인키를 생성할 수 있습니다.

이 키로 공개키를 만들고, 주소를 생성하고, 트랜잭션에 서명할 수 있습니다. 중요한 것은 이 키를 안전하게 보관하는 것입니다.

종이에 적어서 금고에 보관하거나, 하드웨어 지갑에 저장하거나, 암호화된 형태로 백업하세요.

실전 팁

💡 절대 Math.random()을 사용하지 마세요. 이것은 예측 가능한 의사 난수 생성기(PRNG)로, 공격자가 패턴을 분석해서 키를 추측할 수 있습니다. 반드시 crypto.randomBytes()를 사용하세요.

💡 브라우저 환경에서는 window.crypto.getRandomValues()를 사용하세요. 이것도 암호학적으로 안전한 난수를 제공합니다.

💡 개인키를 BIP32/BIP39 같은 계층적 결정론적 지갑(HD Wallet) 방식으로 관리하면 하나의 시드에서 여러 키를 파생시킬 수 있습니다. 이렇게 하면 12개 단어(니모닉)만 백업하면 됩니다.

💡 개발 중에는 환경변수에 테스트용 개인키를 저장해서 사용하되, 절대 실제 자산이 있는 키를 코드나 설정 파일에 넣지 마세요.

💡 개인키를 WIF(Wallet Import Format) 형식으로 인코딩하면 체크섬이 포함되어 타이핑 오류를 감지할 수 있습니다. 프로덕션에서는 이런 표준 형식을 사용하는 것이 좋습니다.


4. 공개키_도출하기

시작하며

여러분이 개인키를 생성했다면, 이제 그것으로 무엇을 해야 할까요? 다음 단계는 공개키를 만드는 것입니다.

이 과정은 수학적으로 일방향 함수라서 되돌릴 수 없습니다. 공개키 도출은 타원곡선의 점 곱셈(scalar multiplication) 연산을 사용합니다.

개인키를 k라고 하면, 공개키 P = k × G입니다. 여기서 G는 secp256k1의 생성점입니다.

이 연산의 핵심은 k와 P를 알아도 역산이 불가능하다는 것입니다. 바로 이것이 타원곡선 이산 로그 문제(ECDLP)의 어려움이고, 비트코인 보안의 수학적 기반입니다.

개요

간단히 말해서, 공개키는 개인키를 타원곡선의 생성점 G에 곱한 결과입니다. 이 과정이 안전한 이유는 곱셈은 쉽지만 나눗셈(역산)은 거의 불가능하기 때문입니다.

예를 들어, 개인키가 5라면 공개키는 G+G+G+G+G입니다. 하지만 실제로는 256비트 숫자이므로 엄청나게 많은 덧셈이 필요한데, "double and add" 알고리즘으로 효율적으로 계산합니다.

전통적인 RSA에서는 큰 수의 거듭제곱을 사용했다면, ECDSA는 타원곡선의 기하학적 연산을 사용합니다. 같은 보안 수준에 필요한 키 길이가 훨씬 짧습니다.

공개키 도출의 핵심 특징은 첫째, 결정론적이라는 점입니다. 같은 개인키는 항상 같은 공개키를 생성합니다.

둘째, 공개키는 (x, y) 좌표 쌍으로 표현되며, 압축 형식과 비압축 형식이 있습니다. 셋째, 공개키에서 개인키를 역산하는 것이 계산상 불가능하므로 안전하게 공개할 수 있습니다.

이러한 특징들이 블록체인에서 공개키를 네트워크에 브로드캐스트해도 안전한 이유입니다.

코드 예제

import { Point } from './ellipticCurve'; // 타원곡선 라이브러리

// secp256k1 생성점 G
const G = new Point(
  BigInt('0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798'),
  BigInt('0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8')
);

function derivePublicKey(privateKeyHex: string): { x: string; y: string } {
  // 개인키를 BigInt로 변환
  const privateKey = BigInt('0x' + privateKeyHex);

  // 공개키 = 개인키 × G (타원곡선 점 곱셈)
  const publicKeyPoint = G.multiply(privateKey);

  // x, y 좌표를 16진수 문자열로 반환
  return {
    x: publicKeyPoint.x.toString(16).padStart(64, '0'),
    y: publicKeyPoint.y.toString(16).padStart(64, '0')
  };
}

설명

이것이 하는 일: derivePublicKey 함수는 개인키로부터 비트코인 공개키를 수학적으로 정확하게 도출합니다. 첫 번째로, 16진수 문자열 형태의 개인키를 BigInt로 변환합니다.

이것이 타원곡선 곱셈의 스칼라(배수) 역할을 합니다. 예를 들어, 개인키가 100이면 G를 100번 더하는 것과 같은데, 실제로는 훨씬 큰 숫자입니다.

그 다음으로, G.multiply(privateKey)가 핵심 연산을 수행합니다. 이 함수는 내부적으로 "double and add" 알고리즘을 사용합니다.

즉, 개인키를 이진수로 변환해서 비트마다 점을 두 배로 만들고(doubling), 비트가 1이면 원래 점을 더하는(adding) 방식입니다. 이렇게 하면 256비트 숫자도 최대 256번의 doubling과 128번 정도의 adding으로 계산할 수 있어서 효율적입니다.

마지막으로, 결과 점의 x와 y 좌표를 추출해서 각각 64자리 16진수 문자열로 변환합니다. 비트코인에서 비압축 공개키는 0x04 + x + y 형태로 표현되고(총 65바이트), 압축 공개키는 0x02 또는 0x03 + x 형태로 표현됩니다(총 33바이트).

y의 홀짝성에 따라 0x02(짝수) 또는 0x03(홀수)을 사용하는데, 타원곡선 방정식에서 y²를 알면 y를 복원할 수 있기 때문입니다. 여러분이 이 함수를 사용하면 개인키와 수학적으로 연결된 공개키를 얻을 수 있습니다.

이 공개키로 비트코인 주소를 만들고, 다른 사람들이 여러분에게 송금할 수 있게 됩니다. 또한 트랜잭션 서명 시 이 공개키를 포함시켜서 네트워크가 서명을 검증할 수 있게 합니다.

공개키는 말 그대로 공개되어도 안전하므로, 블록체인에 영구적으로 기록되어도 문제없습니다.

실전 팁

💡 공개키 압축 형식을 사용하면 트랜잭션 크기를 줄일 수 있어 수수료가 절감됩니다. 최신 비트코인 지갑은 대부분 압축 공개키를 기본으로 사용합니다.

💡 타원곡선 점 곱셈은 CPU 집약적이므로, 가능하면 결과를 캐싱하세요. 같은 개인키로 여러 번 공개키를 계산할 필요는 없습니다.

💡 공개키 좌표가 곡선 방정식 y² = x³ + 7 (mod p)을 만족하는지 검증하는 것이 좋습니다. 이렇게 하면 계산 오류나 손상된 데이터를 감지할 수 있습니다.

💡 실무에서는 noble-secp256k1이나 secp256k1-node 같은 검증된 라이브러리를 사용하세요. 이들은 타이밍 공격(timing attack) 방지 같은 보안 최적화가 되어 있습니다.

💡 여러 공개키를 일괄 생성해야 한다면 WebWorker나 worker_threads를 사용해 병렬 처리하면 성능이 크게 향상됩니다.


5. 지갑_주소_생성

시작하며

여러분이 비트코인을 받으려면 누군가에게 무엇을 알려줘야 할까요? 공개키?

아닙니다. 바로 지갑 주소입니다.

이것은 공개키를 더 짧고 안전하게 만든 형태입니다. 지갑 주소는 공개키를 여러 번 해시 처리하고 체크섬을 추가한 후 Base58로 인코딩한 문자열입니다.

이 과정을 거치면 공개키보다 짧으면서도 오타를 감지할 수 있는 주소가 만들어집니다. 실제로 비트코인 네트워크에서 트랜잭션을 보낼 때 공개키가 노출되는 것은 서명을 검증할 때뿐입니다.

주소만으로는 공개키를 알 수 없어서 양자 컴퓨터 공격에도 한 단계 더 안전합니다.

개요

간단히 말해서, 비트코인 주소는 공개키를 SHA-256과 RIPEMD-160으로 해시하고 Base58Check로 인코딩한 문자열입니다. 이 과정이 왜 필요한가 하면, 첫째로 주소가 공개키보다 짧아서(33바이트 → 20바이트) 타이핑하기 쉽습니다.

둘째로 체크섬이 포함되어 있어 오타를 99.99%이상 감지할 수 있습니다. 예를 들어, 여러분이 주소를 잘못 입력하면 지갑이 즉시 "유효하지 않은 주소"라고 알려줍니다.

전통적인 은행 계좌번호는 중앙 시스템에서 관리되지만, 비트코인 주소는 순수하게 수학적으로 생성되고 누구도 소유권을 주장할 수 없습니다. 개인키를 가진 사람만이 증명할 수 있습니다.

주소 생성의 핵심 특징은 첫째, 이중 해싱(SHA-256 후 RIPEMD-160)을 사용해 충돌 가능성을 최소화한다는 점입니다. 둘째, Base58 인코딩은 비슷해 보이는 문자(0과 O, l과 1 등)를 제외해서 혼동을 방지합니다.

셋째, 버전 바이트를 포함해서 주소 타입(메인넷, 테스트넷 등)을 구분합니다. 이러한 특징들이 비트코인 주소를 실용적이고 안전하게 만들어줍니다.

코드 예제

import crypto from 'crypto';
import bs58 from 'bs58';

function publicKeyToAddress(publicKeyX: string, publicKeyY: string): string {
  // 1. 비압축 공개키 생성 (0x04 + x + y)
  const publicKey = '04' + publicKeyX + publicKeyY;
  const publicKeyBuffer = Buffer.from(publicKey, 'hex');

  // 2. SHA-256 해싱
  const sha256Hash = crypto.createHash('sha256').update(publicKeyBuffer).digest();

  // 3. RIPEMD-160 해싱
  const ripemd160Hash = crypto.createHash('ripemd160').update(sha256Hash).digest();

  // 4. 버전 바이트 추가 (메인넷은 0x00)
  const versionedHash = Buffer.concat([Buffer.from([0x00]), ripemd160Hash]);

  // 5. 체크섬 생성 (double SHA-256의 첫 4바이트)
  const checksum = crypto.createHash('sha256').update(versionedHash).digest();
  const doubleChecksum = crypto.createHash('sha256').update(checksum).digest().slice(0, 4);

  // 6. Base58 인코딩
  const address = bs58.encode(Buffer.concat([versionedHash, doubleChecksum]));

  return address;
}

설명

이것이 하는 일: publicKeyToAddress 함수는 공개키로부터 표준 비트코인 P2PKH(Pay-to-PubKey-Hash) 주소를 생성합니다. 첫 번째로, 공개키의 x와 y 좌표를 결합해서 비압축 형식(0x04 접두사 + 64바이트 x + 64바이트 y)을 만듭니다.

그 다음 SHA-256 해시 함수를 적용합니다. SHA-256은 어떤 길이의 입력이든 256비트(32바이트) 출력을 만들어냅니다.

이것이 첫 번째 보안 레이어입니다. 그 다음으로, SHA-256의 결과에 RIPEMD-160을 적용합니다.

RIPEMD-160은 160비트(20바이트) 해시를 생성하는데, 이것이 주소를 더 짧게 만들어줍니다. 왜 두 개의 해시 함수를 사용할까요?

하나의 함수에 취약점이 발견되어도 다른 함수가 보호해주는 방어-in-depth 전략입니다. 그 다음은 버전 바이트 추가입니다.

0x00은 비트코인 메인넷 P2PKH 주소를 의미합니다. 테스트넷은 0x6F를 사용합니다.

이렇게 하면 잘못된 네트워크로 송금하는 실수를 방지할 수 있습니다. 그 후 체크섬을 계산하는데, 버전 바이트 + 해시값을 이중 SHA-256 처리한 결과의 첫 4바이트를 사용합니다.

마지막으로, Base58 인코딩을 수행합니다. Base64와 달리 Base58은 혼동하기 쉬운 문자(0, O, I, l)와 특수문자(+, /)를 제외합니다.

이렇게 만들어진 주소는 보통 "1"로 시작하며(버전 바이트 0x00 때문), 26~35자 길이입니다. 여러분이 이 함수로 생성한 주소를 다른 사람에게 알려주면, 그들은 비트코인을 보낼 수 있습니다.

주소를 받은 사람이 송금하면, 여러분의 개인키로 서명해야만 그 비트코인을 사용할 수 있습니다. 주소에는 체크섬이 있어서 타이핑 오류의 99.99% 이상을 감지하므로, 잘못된 주소로 송금할 위험이 거의 없습니다.

실전 팁

💡 최신 비트코인 지갑은 SegWit 주소(bech32 형식, bc1로 시작)를 권장합니다. 이것은 더 낮은 수수료와 오타 감지 능력이 향상되었습니다.

💡 주소를 한 번만 사용하는 것이 프라이버시에 좋습니다. HD 지갑을 사용하면 트랜잭션마다 새로운 주소를 자동으로 생성할 수 있습니다.

💡 QR 코드로 주소를 공유하면 타이핑 오류를 완전히 방지할 수 있습니다. 대부분의 지갑이 QR 스캔을 지원합니다.

💡 체크섬 검증은 클라이언트 측에서 반드시 수행하세요. 사용자가 주소를 입력할 때 즉시 피드백을 주면 UX가 크게 향상됩니다.

💡 같은 공개키로 여러 주소 형식을 만들 수 있습니다(P2PKH, P2SH, Bech32). 각각 장단점이 있으므로 사용 사례에 맞게 선택하세요.


6. 디지털_서명_생성

시작하며

여러분이 비트코인을 전송하려고 할 때, 네트워크는 어떻게 그것이 정말 여러분의 의도인지 확인할까요? 바로 디지털 서명입니다.

이것은 손으로 쓰는 서명의 디지털 버전입니다. ECDSA(Elliptic Curve Digital Signature Algorithm) 서명은 메시지와 개인키로부터 (r, s)라는 두 개의 숫자를 생성합니다.

이 서명은 해당 개인키로만 만들 수 있지만, 누구나 공개키로 검증할 수 있습니다. 2010년에는 플레이스테이션3의 ECDSA 구현에서 k 값을 재사용하는 실수로 인해 개인키가 노출된 사건이 있었습니다.

바로 이것이 서명 생성에서 무작위성이 얼마나 중요한지 보여줍니다.

개요

간단히 말해서, ECDSA 서명은 메시지 해시, 개인키, 그리고 무작위 k 값을 사용해 (r, s) 쌍을 생성하는 알고리즘입니다. 이 서명이 안전한 이유는 개인키를 알아야만 유효한 서명을 만들 수 있기 때문입니다.

예를 들어, 여러분이 "Alice에게 1 BTC를 보냄"이라는 트랜잭션을 만들면, 여러분의 개인키로 서명해야 네트워크가 받아들입니다. 공격자가 메시지를 변조하거나 위조 서명을 만들 수 없습니다.

전통적인 RSA 서명은 큰 수의 거듭제곱을 사용했지만, ECDSA는 타원곡선 연산을 사용합니다. 더 작은 키와 서명 크기로 같은 보안을 제공합니다.

ECDSA 서명의 핵심 특징은 첫째, k 값이 매번 새롭고 무작위여야 한다는 점입니다. k를 재사용하거나 예측 가능하면 개인키가 노출됩니다.

둘째, 서명은 결정론적 k 생성(RFC 6979)을 사용할 수도 있어서 테스트가 쉽습니다. 셋째, 서명 크기가 고정되어 있어(보통 70-72바이트) 트랜잭션 크기 계산이 예측 가능합니다.

이러한 특징들이 비트코인의 모든 트랜잭션을 안전하게 만들어줍니다.

코드 예제

import crypto from 'crypto';
import { Point } from './ellipticCurve';

const N = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141');
const G = /* 생성점 */;

function signMessage(messageHash: string, privateKeyHex: string): { r: string; s: string } {
  const z = BigInt('0x' + messageHash); // 메시지 해시
  const d = BigInt('0x' + privateKeyHex); // 개인키

  let r: bigint, s: bigint;

  do {
    // 무작위 k 생성 (매우 중요!)
    const k = BigInt('0x' + crypto.randomBytes(32).toString('hex')) % N;

    // r = (k × G)의 x 좌표 mod n
    const point = G.multiply(k);
    r = point.x % N;

    // s = k⁻¹ × (z + r × d) mod n
    const kInv = modInverse(k, N);
    s = (kInv * (z + r * d)) % N;

  } while (r === 0n || s === 0n); // 유효성 검증

  return {
    r: r.toString(16).padStart(64, '0'),
    s: s.toString(16).padStart(64, '0')
  };
}

설명

이것이 하는 일: signMessage 함수는 비트코인 트랜잭션에 서명하는 표준 ECDSA 알고리즘을 구현합니다. 첫 번째로, 메시지 해시 z와 개인키 d를 BigInt로 변환합니다.

비트코인에서 "메시지"는 보통 트랜잭션 데이터의 SHA-256 해시입니다. 해시를 사용하는 이유는 임의 길이의 메시지를 고정 크기 숫자로 변환하기 위해서입니다.

그 다음으로, 암호학적으로 안전한 무작위 k를 생성합니다. 이 k는 서명마다 반드시 달라야 합니다.

만약 두 서명에서 같은 k를 사용하면, 공격자가 두 서명을 비교해서 개인키를 계산할 수 있습니다. 이것이 바로 플레이스테이션3 해킹과 일부 비트코인 도난 사건의 원인이었습니다.

r 값을 계산할 때는 k × G(k와 생성점 G의 타원곡선 곱셈)를 수행하고, 그 결과 점의 x 좌표를 n으로 나눈 나머지를 사용합니다. 그 다음 s 값은 s = k⁻¹ × (z + r × d) mod n 공식으로 계산합니다.

여기서 k⁻¹은 k의 모듈러 역원(modular inverse)입니다. 이 공식이 ECDSA의 핵심 수학입니다.

마지막으로, r과 s가 모두 0이 아닌지 확인합니다. 통계적으로 거의 발생하지 않지만, 프로토콜 규격상 0은 유효하지 않으므로 다시 생성해야 합니다.

최종적으로 (r, s) 쌍을 16진수 문자열로 반환하는데, 이것이 DER 또는 Compact 형식으로 인코딩되어 트랜잭션에 포함됩니다. 여러분이 이 함수로 생성한 서명을 트랜잭션에 포함시키면, 네트워크의 모든 노드가 여러분의 공개키로 검증할 수 있습니다.

서명이 유효하면 트랜잭션이 블록에 포함되고, 비트코인이 전송됩니다. 서명은 위조할 수 없고, 메시지가 조금이라도 변조되면 검증이 실패하므로, 블록체인의 무결성이 보장됩니다.

실전 팁

💡 프로덕션에서는 RFC 6979 결정론적 k 생성을 사용하는 것이 안전합니다. 같은 메시지와 개인키는 항상 같은 k를 만들어서 k 재사용 위험이 없습니다.

💡 서명의 s 값이 n/2보다 크면 n-s로 바꾸는 "낮은 s" 규칙을 사용하세요. 비트코인 Core 0.11.1부터 이것이 표준이며, 트랜잭션 변조 공격을 방지합니다.

💡 k 값은 절대 재사용하지 마세요. 두 개의 서명에서 같은 k를 사용하면 간단한 대수학으로 개인키를 계산할 수 있습니다.

💡 하드웨어 지갑을 사용하면 서명 과정이 안전한 칩 안에서 일어나므로, 개인키가 절대 컴퓨터로 노출되지 않습니다.

💡 대량의 서명을 생성해야 한다면, WebAssembly로 컴파일된 암호화 라이브러리를 사용하면 성능이 10배 이상 향상될 수 있습니다.


7. 서명_검증하기

시작하며

여러분이 비트코인 네트워크의 노드를 운영한다면, 받은 트랜잭션이 정말 유효한지 어떻게 확인할까요? 바로 서명 검증입니다.

이것은 서명 생성의 역과정입니다. 서명 검증은 메시지 해시, 서명 (r, s), 그리고 공개키만 있으면 됩니다.

개인키는 전혀 필요 없습니다. 이것이 바로 공개키 암호화의 아름다움입니다.

비트코인 네트워크는 초당 수천 개의 서명을 검증해야 하므로, 이 과정의 효율성이 매우 중요합니다. 검증이 빠르면 블록 전파가 빨라지고 네트워크가 더 안정적입니다.

개요

간단히 말해서, ECDSA 서명 검증은 공개키와 서명 (r, s)를 사용해 서명이 유효한지 수학적으로 확인하는 과정입니다. 검증이 작동하는 원리는 타원곡선의 수학적 특성 덕분입니다.

서명 생성 시 사용한 k × G의 결과가, 검증 계산으로 재구성한 점과 일치하는지 확인합니다. 예를 들어, 올바른 개인키로 서명했다면 검증 공식이 정확히 같은 r 값을 만들어냅니다.

전통적인 인증 시스템에서는 서버에 저장된 정보와 비교했지만, 블록체인에서는 순수하게 수학적 검증만으로 충분합니다. 중앙 데이터베이스가 필요 없습니다.

서명 검증의 핵심 특징은 첫째, 공개키만으로 검증 가능하므로 누구나 독립적으로 확인할 수 있다는 점입니다. 둘째, 검증은 생성보다 약간 느리지만 여전히 빠릅니다(밀리초 단위).

셋째, 서명이나 메시지가 조금이라도 변조되면 검증이 실패합니다. 이러한 특징들이 블록체인을 신뢰 불필요한(trustless) 시스템으로 만들어줍니다.

코드 예제

import { Point } from './ellipticCurve';

const N = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141');
const G = /* 생성점 */;

function verifySignature(
  messageHash: string,
  signature: { r: string; s: string },
  publicKey: { x: string; y: string }
): boolean {
  const z = BigInt('0x' + messageHash);
  const r = BigInt('0x' + signature.r);
  const s = BigInt('0x' + signature.s);

  // 공개키 점 생성
  const Q = new Point(BigInt('0x' + publicKey.x), BigInt('0x' + publicKey.y));

  // u1 = z × s⁻¹ mod n, u2 = r × s⁻¹ mod n
  const sInv = modInverse(s, N);
  const u1 = (z * sInv) % N;
  const u2 = (r * sInv) % N;

  // (x, y) = u1 × G + u2 × Q
  const point = G.multiply(u1).add(Q.multiply(u2));

  // 검증: point.x mod n === r
  return (point.x % N) === r;
}

설명

이것이 하는 일: verifySignature 함수는 주어진 서명이 특정 공개키와 메시지에 대해 유효한지 확인합니다. 첫 번째로, 메시지 해시 z와 서명의 r, s 값을 BigInt로 변환합니다.

그 다음 공개키의 x, y 좌표로 타원곡선 상의 점 Q를 생성합니다. 이 Q는 서명을 생성할 때 사용된 개인키 d에 대해 Q = d × G 관계를 만족합니다.

그 다음으로, s의 모듈러 역원(sInv)을 계산하고, 이것을 사용해 u1 = z × sInv와 u2 = r × sInv를 계산합니다. 이 두 값이 검증 공식의 핵심입니다.

왜 이렇게 계산하는지는 ECDSA의 수학적 증명에서 나오는데, 간단히 말하면 서명 생성 공식 s = k⁻¹ × (z + r × d)를 역으로 풀어서 k × G를 복원하기 위함입니다. 핵심 계산은 point = u1 × G + u2 × Q입니다.

이것이 서명 생성 시 사용된 k × G를 재구성합니다. 수학적으로, 올바른 서명이라면 이 point의 x 좌표가 서명의 r 값과 정확히 일치합니다.

따라서 마지막 단계에서 point.x % N === r을 비교해서 true면 유효한 서명, false면 위조되었거나 잘못된 서명입니다. 여러분이 이 함수를 사용하면 비트코인 트랜잭션의 유효성을 검증할 수 있습니다.

네트워크의 모든 풀 노드가 이 검증을 수행해서, 잘못된 트랜잭션이 블록에 포함되는 것을 방지합니다. 검증은 빠르고(보통 1ms 이하) 결정론적이므로, 전 세계의 모든 노드가 같은 결론에 도달합니다.

이것이 합의(consensus)의 기반입니다.

실전 팁

💡 서명 검증 전에 r과 s가 유효 범위(1 <= r, s < n)에 있는지 확인하세요. 범위 밖의 값은 즉시 거부해야 합니다.

💡 공개키가 실제로 타원곡선 상의 점인지 검증하세요(y² = x³ + 7 mod p). 악의적인 공개키로 인한 공격을 방지할 수 있습니다.

💡 배치 검증(batch verification)을 사용하면 여러 서명을 동시에 검증할 때 30-40% 성능 향상이 가능합니다. 블록 전체를 검증할 때 유용합니다.

💡 캐시를 활용하세요. 같은 공개키로 여러 서명을 검증한다면, 공개키 점 Q를 재사용하면 됩니다.

💡 Schnorr 서명(BIP 340)은 ECDSA보다 간단하고 빠르며 배치 검증에 더 유리합니다. 비트코인 Taproot 업그레이드에서 도입되었습니다.


8. 해시_함수의_역할

시작하며

여러분이 블록체인 코드를 보면 SHA-256, RIPEMD-160 같은 해시 함수가 도처에 있습니다. 왜 이렇게 많이 사용될까요?

해시 함수는 블록체인의 접착제 같은 역할을 합니다. 해시 함수는 임의 길이의 데이터를 고정 길이의 출력으로 변환하는 일방향 함수입니다.

"일방향"이라는 것이 핵심인데, 출력에서 입력을 역산하는 것이 거의 불가능하기 때문입니다. 비트코인에서 해시 함수는 블록 ID, 트랜잭션 ID, 주소 생성, 작업 증명 등 거의 모든 곳에 사용됩니다.

해시 함수 없이는 블록체인이 존재할 수 없습니다.

개요

간단히 말해서, 암호학적 해시 함수는 데이터의 "지문"을 만들어내는 수학적 함수입니다. 좋은 해시 함수의 조건은 첫째, 같은 입력은 항상 같은 출력을 만들어냅니다(결정론적).

둘째, 조금이라도 다른 입력은 완전히 다른 출력을 만듭니다(눈사태 효과). 셋째, 출력에서 입력을 역산하는 것이 불가능합니다(일방향성).

예를 들어, "hello"의 SHA-256 해시는 항상 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824이지만, 이 해시값만 보고 원본이 "hello"임을 알아내는 것은 불가능합니다. 전통적인 체크섬은 데이터 손상 감지용이었지만, 암호학적 해시는 의도적인 변조도 감지할 수 있습니다.

공격자가 같은 해시를 만드는 다른 데이터를 찾는 것이 불가능하기 때문입니다. 해시 함수의 핵심 특징은 첫째, SHA-256은 256비트(32바이트) 고정 출력을 만들어 예측 가능한 크기를 제공합니다.

둘째, 충돌(서로 다른 입력이 같은 출력을 만드는 것)을 찾는 것이 계산상 불가능합니다. 셋째, 빠르게 계산할 수 있어서 성능에 영향을 적게 줍니다.

이러한 특징들이 블록체인의 무결성과 보안을 보장합니다.

코드 예제

import crypto from 'crypto';

// SHA-256 해싱 (가장 많이 사용)
function sha256(data: string | Buffer): string {
  return crypto.createHash('sha256')
    .update(data)
    .digest('hex');
}

// RIPEMD-160 해싱 (주소 생성에 사용)
function ripemd160(data: string | Buffer): string {
  return crypto.createHash('ripemd160')
    .update(data)
    .digest('hex');
}

// 이중 SHA-256 (비트코인 블록/트랜잭션 ID)
function hash256(data: string | Buffer): string {
  const firstHash = crypto.createHash('sha256').update(data).digest();
  return crypto.createHash('sha256').update(firstHash).digest('hex');
}

// SHA-256 + RIPEMD-160 (주소 생성용)
function hash160(data: string | Buffer): string {
  const sha = crypto.createHash('sha256').update(data).digest();
  return crypto.createHash('ripemd160').update(sha).digest('hex');
}

설명

이것이 하는 일: 이 함수들은 비트코인 프로토콜에서 사용되는 모든 표준 해싱 패턴을 구현합니다. 첫 번째로, sha256 함수는 가장 기본적인 SHA-256 해싱을 수행합니다.

Node.js의 crypto 모듈을 사용하며, 문자열이나 Buffer를 입력받아 64자리 16진수 문자열을 반환합니다. SHA-256은 미국 국가안보국(NSA)이 설계했으며, 현재까지 실용적인 충돌 공격이 발견되지 않은 안전한 해시 함수입니다.

그 다음으로, ripemd160은 160비트 해시를 생성합니다. SHA-256보다 출력이 짧아서 주소 크기를 줄이는 데 사용됩니다.

RIPEMD-160은 유럽에서 개발된 해시 함수로, SHA-256과 다른 설계 원리를 가지고 있어서 함께 사용하면 방어-in-depth 효과가 있습니다. hash256 함수는 비트코인의 "이중 SHA-256" 패턴입니다.

데이터를 SHA-256으로 해시하고, 그 결과를 다시 SHA-256으로 해시합니다. 모든 블록 ID와 트랜잭션 ID가 이 방식으로 생성됩니다.

이중 해싱을 사용하는 이유는 "length extension attack" 같은 특정 공격을 방지하기 위해서입니다. 마지막으로, hash160은 공개키를 주소로 변환할 때 사용하는 패턴입니다.

먼저 SHA-256을 적용하고, 그 결과에 RIPEMD-160을 적용합니다. 이렇게 두 개의 서로 다른 해시 함수를 조합하면, 하나의 함수에 취약점이 발견되어도 다른 함수가 보호해줍니다.

여러분이 이 함수들을 사용하면 비트코인 프로토콜과 완벽히 호환되는 해싱을 수행할 수 있습니다. 트랜잭션을 만들 때 데이터를 hash256으로 해싱해서 트랜잭션 ID를 만들고, 공개키를 hash160으로 해싱해서 주소를 만들며, 서명할 메시지를 sha256으로 해싱합니다.

이 모든 과정이 결정론적이고 검증 가능해서, 전 세계의 모든 노드가 같은 결과를 얻을 수 있습니다.

실전 팁

💡 해시는 일방향이므로 절대 민감한 데이터(비밀번호 등)를 그대로 해싱하지 마세요. Salt와 키 유도 함수(PBKDF2, bcrypt 등)를 사용해야 합니다.

💡 큰 파일을 해싱할 때는 스트리밍 방식을 사용하세요. 전체를 메모리에 로드하지 않고 청크 단위로 처리할 수 있습니다.

💡 SHA-256은 CPU 집약적이지만, 일부 CPU는 SHA 확장 명령어(SHA-NI)로 하드웨어 가속을 제공합니다. 최신 라이브러리는 자동으로 이를 활용합니다.

💡 해시 출력을 비교할 때는 타이밍 공격을 방지하기 위해 상수 시간 비교(constant-time comparison)를 사용하세요. crypto.timingSafeEqual() 같은 함수를 활용하세요.

💡 SHA-3(Keccak)는 SHA-2와 완전히 다른 설계로 만들어진 차세대 해시 함수입니다. 이더리움에서 사용되며, SHA-2에 문제가 발견될 경우를 대비한 백업 표준입니다.


#TypeScript#Blockchain#ECDSA#Cryptography#PublicKey#typescript

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.