제2장

디지털 서명과 암호화

콘텐츠 진위성과 무결성을 보장하는 암호화 기법

암호화 기초

디지털 서명은 콘텐츠 인증의 핵심 기술입니다. 이는 공개 키 암호화(PKI)를 기반으로 하여 두 가지 핵심 보장을 제공합니다: 진위성(authenticity)과 무결성(integrity)입니다. 진위성은 콘텐츠가 주장된 생성자로부터 실제로 왔다는 것을 증명하고, 무결성은 서명 후 콘텐츠가 변경되지 않았음을 보장합니다.

디지털 서명의 작동 방식을 이해하기 위해서는 먼저 비대칭 암호화의 기본 개념을 파악해야 합니다.

공개 키 암호화 원리

비대칭 암호화는 수학적으로 연결되지만 서로 다른 두 개의 키를 사용합니다:

이 비대칭성은 강력한 보안 속성을 가능하게 합니다. 공개 키를 알고 있어도 개인 키를 계산하는 것은 컴퓨터적으로 불가능합니다.

// 키 쌍 생성 예제 (Ed25519 사용)
import { generateKeyPairSync } from 'crypto';

// Ed25519 키 쌍 생성
const { publicKey, privateKey } = generateKeyPairSync('ed25519', {
    publicKeyEncoding: {
        type: 'spki',
        format: 'pem'
    },
    privateKeyEncoding: {
        type: 'pkcs8',
        format: 'pem'
    }
});

console.log('공개 키:', publicKey);
console.log('개인 키:', privateKey);

해시 함수의 역할

디지털 서명은 전체 콘텐츠에 직접 서명하지 않습니다. 대신 암호화 해시 함수를 사용하여 콘텐츠의 고정 크기 지문을 생성합니다. 이 접근 방식은 효율성과 보안을 모두 제공합니다.

암호화 해시 함수의 속성

알고리즘 출력 크기 보안 상태 권장 사용
MD5 128 비트 ❌ 손상됨 사용 금지
SHA-1 160 비트 ⚠️ 약함 레거시만
SHA-256 256 비트 ✅ 안전함 범용
SHA-512 512 비트 ✅ 매우 안전함 높은 보안
SHA-3 224-512 비트 ✅ 최신 미래 보장
BLAKE3 256 비트 ✅ 고속 대용량 파일
// 다양한 해시 알고리즘으로 콘텐츠 해싱
import { createHash } from 'crypto';
import { readFileSync } from 'fs';

// 이미지 파일 읽기
const imageBuffer = readFileSync('photo.jpg');

// SHA-256 해시 계산
const sha256Hash = createHash('sha256')
    .update(imageBuffer)
    .digest('hex');

// SHA-512 해시 계산
const sha512Hash = createHash('sha512')
    .update(imageBuffer)
    .digest('hex');

// SHA-3 해시 계산
const sha3Hash = createHash('sha3-256')
    .update(imageBuffer)
    .digest('hex');

console.log('SHA-256:', sha256Hash);
console.log('SHA-512:', sha512Hash);
console.log('SHA-3:', sha3Hash);

디지털 서명 알고리즘

여러 디지털 서명 알고리즘이 있으며 각각 고유한 강점과 트레이드오프가 있습니다. WIA-AI-017은 세 가지 주요 알고리즘군을 지원합니다.

RSA (Rivest-Shamir-Adleman)

RSA는 가장 오래되고 널리 배포된 디지털 서명 알고리즘입니다. 큰 소수의 곱셈은 쉽지만 인수분해는 어렵다는 수학적 어려움에 기반합니다.

// RSA 서명 생성 및 검증
import { createSign, createVerify } from 'crypto';

// 콘텐츠 해시 계산
const content = readFileSync('document.pdf');
const hash = createHash('sha256').update(content).digest();

// RSA 서명 생성
const sign = createSign('RSA-SHA256');
sign.update(hash);
const signature = sign.sign(privateKey, 'hex');

// 서명 검증
const verify = createVerify('RSA-SHA256');
verify.update(hash);
const isValid = verify.verify(publicKey, signature, 'hex');

console.log('서명 유효성:', isValid);

ECDSA (Elliptic Curve Digital Signature Algorithm)

ECDSA는 타원 곡선 암호화를 사용하여 RSA보다 훨씬 작은 키로 동등한 보안을 제공합니다. 이는 대역폭과 저장 공간을 절약합니다.

⚠️ 곡선 선택
모든 타원 곡선이 동등하게 안전한 것은 아닙니다. NIST P-256, P-384, P-521 또는 Curve25519와 같은 잘 연구된 곡선을 사용하십시오. 사용자 정의 곡선이나 검증되지 않은 매개변수는 피하십시오.
// ECDSA 서명 (P-256 곡선 사용)
import { generateKeyPairSync, createSign, createVerify } from 'crypto';

// P-256 곡선으로 ECDSA 키 쌍 생성
const { publicKey, privateKey } = generateKeyPairSync('ec', {
    namedCurve: 'P-256',
    publicKeyEncoding: { type: 'spki', format: 'pem' },
    privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

// 콘텐츠에 서명
const content = '인증할 중요한 콘텐츠';
const sign = createSign('SHA256');
sign.update(content);
const signature = sign.sign(privateKey, 'base64');

// 서명 검증
const verify = createVerify('SHA256');
verify.update(content);
const isValid = verify.verify(publicKey, signature, 'base64');

console.log('ECDSA 서명 검증:', isValid);

EdDSA (Edwards-curve Digital Signature Algorithm)

Ed25519는 최신 서명 알고리즘으로 보안, 성능 및 단순성에 최적화되었습니다. 많은 일반적인 구현 실수에 대해 저항력이 있습니다.

✅ WIA-AI-017 권장사항
새로운 구현의 경우 Ed25519를 권장합니다. 이는 우수한 성능, 강력한 보안 및 구현 용이성을 제공합니다. 레거시 호환성이 필요한 경우에만 RSA 또는 ECDSA를 사용하십시오.
// Ed25519 서명 - 권장 접근 방식
import { generateKeyPairSync, sign, verify } from 'crypto';

// Ed25519 키 쌍 생성
const { publicKey, privateKey } = generateKeyPairSync('ed25519');

// 콘텐츠 준비
const content = Buffer.from('인증할 이미지 데이터');

// 서명 생성 (해싱이 내부적으로 처리됨)
const signature = sign(null, content, privateKey);

// 서명 검증
const isValid = verify(null, content, publicKey, signature);

console.log('Ed25519 서명 검증:', isValid);
console.log('서명 크기:', signature.length, '바이트');

서명 메타데이터 구조

디지털 서명은 단지 암호화 바이트만이 아닙니다. 검증 가능성을 위해 풍부한 메타데이터와 함께 제공되어야 합니다.

필수 서명 필드

// WIA-AI-017 서명 매니페스트 구조
interface SignatureManifest {
    // 서명 식별
    signatureId: string;              // 고유 서명 ID (UUID)
    version: string;                  // 매니페스트 버전 (예: "1.0")

    // 콘텐츠 참조
    contentHash: string;              // SHA-256 콘텐츠 해시
    contentType: string;              // MIME 타입 (image/jpeg 등)
    contentSize: number;              // 바이트 단위 크기

    // 암호화 정보
    algorithm: string;                // 서명 알고리즘 (Ed25519, ECDSA, RSA)
    hashAlgorithm: string;            // 해시 알고리즘 (SHA-256, SHA-512)
    signature: string;                // Base64 인코딩된 서명
    publicKey: string;                // Base64 인코딩된 공개 키

    // 서명자 정보
    signer: {
        name: string;                 // 서명자 이름/조직
        email?: string;               // 연락 이메일
        did?: string;                 // 분산 식별자 (선택)
    };

    // 타임스탬프
    signedAt: string;                 // ISO 8601 타임스탬프
    expiresAt?: string;               // 서명 만료 (선택)

    // 추가 컨텍스트
    purpose?: string;                 // 서명 목적
    chain?: SignatureManifest[];      // 이전 서명 체인
}

서명 생성 완전한 예제

// 완전한 서명 매니페스트 생성
import { v4 as uuidv4 } from 'uuid';
import { generateKeyPairSync, sign, createHash } from 'crypto';
import { readFileSync } from 'fs';

class ContentSigner {
    private privateKey: Buffer;
    private publicKey: Buffer;

    constructor() {
        // 키 쌍 생성
        const keys = generateKeyPairSync('ed25519');
        this.privateKey = keys.privateKey;
        this.publicKey = keys.publicKey;
    }

    signContent(filePath: string, signerName: string): SignatureManifest {
        // 콘텐츠 읽기
        const content = readFileSync(filePath);

        // 콘텐츠 해시 계산
        const contentHash = createHash('sha256')
            .update(content)
            .digest('base64');

        // 서명 생성
        const signature = sign(null, content, this.privateKey);

        // 매니페스트 구성
        const manifest: SignatureManifest = {
            signatureId: uuidv4(),
            version: '1.0',
            contentHash: contentHash,
            contentType: 'image/jpeg',
            contentSize: content.length,
            algorithm: 'Ed25519',
            hashAlgorithm: 'SHA-256',
            signature: signature.toString('base64'),
            publicKey: this.publicKey.toString('base64'),
            signer: {
                name: signerName,
                email: 'signer@example.com'
            },
            signedAt: new Date().toISOString(),
            purpose: 'AI 콘텐츠 인증'
        };

        return manifest;
    }
}

// 사용 예제
const signer = new ContentSigner();
const manifest = signer.signContent('photo.jpg', 'WIA 콘텐츠 연구소');
console.log('서명 매니페스트:', JSON.stringify(manifest, null, 2));

서명 검증 프로세스

서명 생성보다 검증이 더 중요합니다. 검증 프로세스는 여러 단계를 거쳐 진위성과 무결성을 모두 확인해야 합니다.

검증 단계

  1. 매니페스트 파싱: 서명 메타데이터의 유효성 검사
  2. 타임스탬프 검사: 서명이 만료되지 않았는지 확인
  3. 콘텐츠 해싱: 현재 콘텐츠 해시 재계산
  4. 해시 비교: 매니페스트의 해시와 비교
  5. 공개 키 복구: Base64 공개 키 디코딩
  6. 서명 검증: 암호화 검증 수행
  7. 신뢰 체인 검사: 서명자 인증 검증
// 서명 검증 구현
import { verify, createHash } from 'crypto';
import { readFileSync } from 'fs';

class SignatureVerifier {
    verifyContent(filePath: string, manifest: SignatureManifest): boolean {
        try {
            // 1. 콘텐츠 읽기
            const content = readFileSync(filePath);

            // 2. 콘텐츠 해시 재계산
            const calculatedHash = createHash('sha256')
                .update(content)
                .digest('base64');

            // 3. 해시 무결성 검사
            if (calculatedHash !== manifest.contentHash) {
                console.error('❌ 콘텐츠 해시 불일치');
                return false;
            }

            // 4. 만료 시간 검사
            if (manifest.expiresAt) {
                const expiry = new Date(manifest.expiresAt);
                if (expiry < new Date()) {
                    console.error('❌ 서명 만료됨');
                    return false;
                }
            }

            // 5. 공개 키 및 서명 디코딩
            const publicKey = Buffer.from(manifest.publicKey, 'base64');
            const signature = Buffer.from(manifest.signature, 'base64');

            // 6. 암호화 서명 검증
            const isValid = verify(null, content, publicKey, signature);

            if (isValid) {
                console.log('✅ 서명 검증 성공');
                console.log('서명자:', manifest.signer.name);
                console.log('서명 시각:', manifest.signedAt);
            }

            return isValid;

        } catch (error) {
            console.error('❌ 검증 오류:', error.message);
            return false;
        }
    }
}

// 사용 예제
const verifier = new SignatureVerifier();
const manifest = JSON.parse(readFileSync('signature.json', 'utf-8'));
const isValid = verifier.verifyContent('photo.jpg', manifest);

타임스탬프 서비스

신뢰할 수 있는 타임스탬프는 콘텐츠가 특정 시점에 존재했음을 증명합니다. 이는 날짜 조작을 방지하고 법적 비거부성을 제공합니다.

RFC 3161 타임스탬프 프로토콜

RFC 3161은 디지털 타임스탬프를 위한 표준 프로토콜을 정의합니다. TSA(Time Stamping Authority)는 콘텐츠 해시에 서명하고 정확한 시간을 인증합니다.

// RFC 3161 타임스탬프 요청
import axios from 'axios';
import { createHash } from 'crypto';

async function requestTimestamp(content: Buffer): Promise {
    // 콘텐츠 해시 생성
    const hash = createHash('sha256').update(content).digest();

    // TSA 엔드포인트
    const tsaUrl = 'https://freetsa.org/tsr';

    // TSR (타임스탬프 요청) 생성
    const tsrRequest = {
        version: 1,
        messageImprint: {
            hashAlgorithm: 'SHA-256',
            hashedMessage: hash.toString('base64')
        },
        nonce: Math.floor(Math.random() * 1000000),
        certReq: true
    };

    // TSA에 요청 전송
    const response = await axios.post(tsaUrl, tsrRequest, {
        headers: { 'Content-Type': 'application/timestamp-query' },
        responseType: 'arraybuffer'
    });

    // TST (타임스탬프 토큰) 반환
    return Buffer.from(response.data).toString('base64');
}

// 사용 예제
const content = readFileSync('document.pdf');
const timestamp = await requestTimestamp(content);
console.log('타임스탬프 토큰:', timestamp);

키 관리 및 순환

암호화 키는 조직의 가장 중요한 자산입니다. 적절한 키 관리는 장기적인 보안에 필수적입니다.

키 저장 모범 사례

⚠️ 절대 하지 말아야 할 것
❌ 개인 키를 버전 관리(Git)에 커밋
❌ 평문으로 개인 키 이메일 전송
❌ 하드코딩된 키를 소스 코드에 삽입
❌ 백업 없이 키 생성
❌ 여러 환경에서 동일한 키 공유

키 순환 전략

정기적인 키 순환은 손상의 영향을 제한합니다. WIA-AI-017은 다음 순환 정책을 권장합니다:

// 키 순환 시스템
class KeyRotationManager {
    private keyStore: Map = new Map();
    private currentKeyId: string;

    // 새 키 생성 및 활성화
    rotateKeys(): string {
        const newKeyId = `key-${Date.now()}`;
        const { publicKey, privateKey } = generateKeyPairSync('ed25519');

        // 새 키 저장
        this.keyStore.set(newKeyId, { publicKey, privateKey });

        // 이전 키는 검증용으로만 유지
        const oldKeyId = this.currentKeyId;
        if (oldKeyId) {
            const oldKey = this.keyStore.get(oldKeyId);
            // 이전 개인 키는 안전하게 보관만 (서명에는 사용 안함)
            console.log(`이전 키 ${oldKeyId}는 검증 전용`);
        }

        // 현재 키 업데이트
        this.currentKeyId = newKeyId;

        console.log(`✅ 새 키 활성화: ${newKeyId}`);
        return newKeyId;
    }

    // 서명에 현재 키 사용
    sign(content: Buffer): { signature: string; keyId: string } {
        const key = this.keyStore.get(this.currentKeyId);
        const signature = sign(null, content, key.privateKey);

        return {
            signature: signature.toString('base64'),
            keyId: this.currentKeyId
        };
    }

    // 모든 키로 검증 (이전 서명 지원)
    verify(content: Buffer, signature: string, keyId: string): boolean {
        const key = this.keyStore.get(keyId);
        if (!key) {
            console.error('키를 찾을 수 없음:', keyId);
            return false;
        }

        const sig = Buffer.from(signature, 'base64');
        return verify(null, content, key.publicKey, sig);
    }
}

분산 신뢰 모델

중앙 집중식 PKI에만 의존하는 대신 WIA-AI-017은 분산 신뢰 모델을 지원합니다.

Web of Trust

PGP/GPG 모델에서 사용자는 서로의 공개 키를 직접 검증하고 서명합니다. 신뢰는 소셜 그래프를 통해 전이됩니다.

분산 식별자(DID)

DID는 블록체인 또는 분산 원장에 등록된 자체 주권적 디지털 신원을 제공합니다.

// DID 기반 서명자 정보
{
    "signer": {
        "did": "did:wia:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
        "didDocument": {
            "@context": "https://www.w3.org/ns/did/v1",
            "id": "did:wia:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
            "publicKey": [{
                "id": "did:wia:...#key-1",
                "type": "Ed25519VerificationKey2020",
                "controller": "did:wia:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
                "publicKeyBase58": "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV"
            }]
        }
    }
}

양자 저항성

양자 컴퓨터는 RSA 및 ECDSA를 포함한 현재 공개 키 암호화 시스템에 위협이 됩니다. WIA-AI-017은 양자 이후 암호화로의 전환을 준비합니다.

양자 이후 서명 알고리즘

ℹ️ 하이브리드 접근 방식
전환 기간 동안 전통적 알고리즘과 양자 이후 알고리즘을 모두 사용하는 하이브리드 서명을 권장합니다. 이는 기존 시스템과의 호환성을 유지하면서 미래 보장을 제공합니다.

📌 요약

📝 복습 문제

  1. 디지털 서명이 제공하는 두 가지 핵심 보장을 설명하십시오.
  2. 공개 키 암호화에서 개인 키와 공개 키의 역할은 무엇입니까?
  3. 암호화 해시 함수의 다섯 가지 필수 속성을 나열하십시오.
  4. Ed25519가 새로운 구현에 권장되는 이유는 무엇입니까?
  5. 서명 매니페스트에 타임스탬프가 중요한 이유를 설명하십시오.
  6. 개인 키 저장 시 절대 하지 말아야 할 다섯 가지는 무엇입니까?
  7. 키 순환이 보안을 향상시키는 방법을 설명하십시오.
  8. 양자 컴퓨터가 현재 디지털 서명 알고리즘에 미치는 위협은 무엇입니까?

弘益人間 (홍익인간) · 널리 인간을 이롭게 하라