디지털 서명이란 무엇인가: 개인 키로 서명하고 공개 키로 검증하는 원리

디지털 서명이란 무엇인가: 개인 키로 서명하고 공개 키로 검증하는 원리

인터넷에서 받은 프로그램이 개발자가 배포한 그대로인지, 로그인 요청이 정말 등록된 기기에서 왔는지, 웹사이트가 접속하려던 서버가 맞는지는 어떻게 확인할까요? 내용을 암호화하면 제삼자가 읽지 못하게 숨길 수는 있지만 누가 만들었고 중간에서 바뀌지 않았는지까지 저절로 증명되지는 않습니다.

이때 사용하는 도구가 디지털 서명(digital signature)입니다. 디지털 서명은 종이에 하는 서명처럼 데이터에 신원을 연결하지만 눈으로 필체를 비교하는 대신 개인 키와 공개 키를 사용해 수학적으로 검증하는데요. TLS의 서버 인증, JWS와 JWT, SSH 로그인, Git 커밋 서명처럼 생각보다 가까운 곳에서 매일 쓰이고 있습니다.

체크섬으로 받은 파일이 원본과 같은지는 비교할 수 있지만, 체크섬까지 공격자가 바꿨다면 파일의 출처를 확인할 수 없습니다. 디지털 서명은 바로 이 출처 검증까지 필요한 자리에서 등장합니다.

이번 글에서는 디지털 서명이 무엇을 보장하고 무엇은 보장하지 않는지 알아보겠습니다. 먼저 암호화와 서명을 구분하고 서명 생성과 검증의 흐름을 살펴볼 텐데요. 이어서 해시와 HMAC의 차이, 공개 키를 신뢰하는 방법, 대표 알고리즘을 차례로 알아보겠습니다.

서명은 무엇을 증명할까요?

앨리스가 밥에게 다음 메시지를 보낸다고 해볼까요?

앨리스의 메시지
밥에게 10만 원을 보냅니다.

공격자가 전송 중에 10만 원을 100만 원으로 고칠 수 있다면 곤란합니다. 밥의 입장에서는 메시지가 앨리스에게서 왔다는 사실도 확인해야 하죠.

앨리스가 개인 키로 메시지에 서명하고 밥이 앨리스의 공개 키로 이를 검증하면 두 가지를 확인할 수 있습니다.

우선 메시지 무결성(integrity)을 확인할 수 있습니다. 서명한 뒤 메시지가 한 글자라도 바뀌면 검증에 실패합니다.

또한 서명의 진위(authenticity)를 확인할 수 있습니다. 공개 키에 대응하는 개인 키를 가진 주체만 유효한 서명을 만들 수 있기 때문입니다. 다만 여기에는 “그 공개 키가 정말 앨리스의 것”이라는 전제가 붙습니다. 이 전제가 빠지면 어떤 일이 생기는지는 뒤에서 다시 살펴보겠습니다.

디지털 서명은 내용을 숨기지는 않습니다. 메시지와 서명값을 함께 보내면 누구나 메시지를 읽을 수 있어요. 기밀성(confidentiality)이 필요하다면 대칭키 암호화 같은 별도의 암호화 수단을 함께 사용해야 합니다.

암호화와 디지털 서명은 어떻게 다를까요?

디지털 서명을 “개인 키로 암호화하고 공개 키로 복호화하는 것”이라고 설명하는 경우가 있습니다. RSA 연산만 단순화해서 보면 그럴듯합니다. 하지만 실제 서명을 설명하기에는 정확하지 않은 비유예요.

서명 알고리즘에는 서명 생성(sign)과 서명 검증(verify)이라는 별도의 연산이 있습니다. 서명 생성은 메시지와 개인 키를 입력받아 서명값을 만들고, 서명 검증은 메시지와 서명값, 공개 키를 입력받아 참인지 거짓인지를 판단합니다. 검증 결과로 원래 메시지가 튀어나오는 복호화 과정이 아닙니다.

서명과 검증의 입출력
서명 생성:
  sign(개인 키, 메시지) ──▶ 서명값

서명 검증:
  verify(공개 키, 메시지, 서명값) ──▶ 성공 또는 실패

서명 전용 알고리즘에는 암호화나 복호화 연산이 아예 없습니다. RSA처럼 암호화와 서명에 모두 활용되는 기반 기술도 실무에서는 목적에 맞는 방식과 인코딩을 따로 사용합니다.

개인 키와 공개 키가 등장한다는 공통점 때문에 디지털 서명을 비대칭키 암호화의 응용으로 묶어 설명하지만 “암호화”와 “서명”은 해결하는 문제가 다른 셈입니다.

디지털 서명은 어떻게 동작할까요?

실제 서명 방식은 보통 메시지를 암호학적 해시 함수에 넣어 고정 길이의 해시값을 만들고, 그 값을 서명 계산에 사용합니다. 큰 파일 전체에 무거운 공개 키 연산을 적용하는 것보다 효율적이고 서명할 데이터의 길이도 일정하게 다룰 수 있기 때문입니다.

flowchart TB
    accTitle: 디지털 서명 생성과 검증 흐름
    accDescr: 서명자는 메시지를 해시하고 개인 키로 서명값을 만든다. 검증자는 받은 메시지의 해시를 다시 계산하고, 서명값과 공개 키를 함께 사용해 서명이 유효한지 확인한다. 성공하면 메시지의 무결성과 서명의 진위를 확인하고, 실패하면 변조되었거나 키 또는 서명이 잘못된 것으로 판단한다.

    subgraph signer["1. 서명자: 서명 생성"]
        direction LR
        message[/"메시지"/] --> signerHash["해시 함수"]
        signerHash --> digest(["메시지 해시"])
        digest --> sign["서명 생성"]
        privateKey(["개인 키"]) --> sign
        sign --> signature(["서명값"])
    end

    message --> packet["메시지와 서명값<br/>전송"]
    signature --> packet

    subgraph verifier["2. 검증자: 서명 검증"]
        direction LR
        packet --> receivedMessage[/"받은 메시지"/]
        receivedMessage --> verifierHash["같은 해시 함수"]
        verifierHash --> receivedDigest(["계산한 해시"])

        packet --> receivedSignature(["받은 서명값"])
        receivedDigest --> verify["서명 검증"]
        receivedSignature --> verify
        publicKey(["공개 키"]) --> verify

        verify --> valid{"유효한 서명인가?"}
        valid -- "예" --> success(["검증 성공"])
        valid -- "아니요" --> failure(["검증 실패"])
    end

    classDef hash stroke:#0284c7,stroke-width:2px
    classDef key stroke:#7c3aed,stroke-width:2px
    classDef decision stroke:#0d9488,stroke-width:3px
    classDef successNode stroke:#16a34a,stroke-width:3px
    classDef failureNode stroke:#e11d48,stroke-width:3px

    class digest,receivedDigest hash
    class privateKey,publicKey key
    class valid decision
    class success successNode
    class failure failureNode

검증하는 쪽은 받은 메시지로 해시값을 다시 계산합니다. 메시지가 조금이라도 바뀌면 해시값이 달라지고, 기존 서명과 맞지 않아 검증이 실패하죠. 덕분에 짧은 값에 대한 서명으로 크기가 큰 메시지의 무결성까지 확인할 수 있습니다.

이 구조가 안전하려면 해시 함수가 충돌 저항성을 갖춰야 합니다. 공격자가 같은 해시값을 내는 다른 메시지를 만들어 낼 수 있다면, 정상 메시지의 서명을 악성 메시지에 옮겨 붙일 수 있기 때문입니다. 오래된 서명 체계에서 SHA-1을 더 이상 사용하지 않는 이유도 알려진 충돌 공격으로 이 전제를 신뢰하기 어려워졌기 때문입니다.

다만 구현할 때 메시지를 직접 해시한 뒤 그 값만 서명 API에 넘겨야 한다는 뜻은 아닙니다. 서명 API와 프로토콜은 사용할 해시 함수와 인코딩 규칙, 서명 대상 바이트를 함께 정합니다. 별도의 사전 해시 방식을 명시한 경우가 아니라면 검증된 라이브러리의 서명 API에 원본 메시지를 전달하고 프로토콜이 요구하는 알고리즘을 정확히 지정하는 편이 안전합니다.

해시와 HMAC, 디지털 서명은 무엇이 다를까요?

세 도구는 모두 데이터가 바뀌었는지 확인하는 데 쓰이지만 키와 검증자의 권한이 다릅니다.

도구사용하는 키누가 검증할 수 있나검증자가 같은 값을 만들 수 있나
암호학적 해시없음누구나누구나
HMAC양쪽이 공유하는 비밀 키비밀 키를 가진 사람가능
디지털 서명개인 키와 공개 키 한 쌍공개 키를 가진 누구나불가능

해시값만 함께 배포하면 공격자가 메시지를 바꾸고 해시값도 다시 계산할 수 있습니다. 해시만으로는 악의적인 변조를 막지 못합니다. 믿을 수 있는 경로에서 받은 예상 해시값이 따로 있어야 하죠.

해시 기반 메시지 인증 코드(Hash-based Message Authentication Code, HMAC)는 공유한 비밀 키와 메시지로 인증값을 만듭니다. 빠르고 간단하지만 검증자도 같은 비밀 키를 가지므로 유효한 인증값을 새로 만들 수 있는데요. 나중에 제삼자에게 “누가 이 값을 만들었는지” 보여 주려 해도 두 참여자 중 누구든 만들 수 있어 구분할 방법이 없습니다.

반면 디지털 서명은 개인 키를 가진 쪽만 만들 수 있고 공개 키만 가진 쪽은 검증만 할 수 있습니다. 바로 이 차이 덕분에 검증 권한은 널리 나눠 주면서 서명 권한은 한곳에 남길 수 있죠.

JWT 서명 알고리즘에서 HS256도 흔히 “서명”이라고 부르지만 엄밀히는 HMAC을 사용한 메시지 인증 코드입니다. RS256, ES256처럼 개인 키로 만들고 공개 키로 검증하는 방식이 암호학에서 말하는 디지털 서명에 해당합니다.

공개 키의 주인은 어떻게 확인할까요?

공격자가 앨리스의 메시지를 지우고 자기 개인 키로 새로운 메시지에 서명한 다음, 자기 공개 키를 “앨리스의 공개 키”라고 건네면 어떻게 될까요? 서명 검증 자체는 성공합니다. 사용한 공개 키의 주인이 누구인지 확인하지 않았기 때문입니다.

디지털 서명은 “이 공개 키에 대응하는 개인 키가 서명했다”는 사실을 보여 줄 뿐입니다. 공개 키와 실제 신원을 연결하는 일은 서명 알고리즘 바깥에서 해결해야 하는데요.

HTTPS 인증서에서는 신뢰할 수 있는 인증 기관(Certificate Authority, CA)이 도메인과 공개 키를 묶어 서명합니다. 브라우저는 인증서의 신뢰 사슬을 따라가며 서버 공개 키의 주인을 확인합니다.

SSH로 GitHub에 접속할 때는 사용자가 자신의 공개 키를 GitHub 계정에 미리 등록합니다. SSH 공개 키 인증을 사용해 접속하면 로컬 컴퓨터가 현재 세션에 묶인 인증 데이터에 개인 키로 서명하고 GitHub가 등록된 공개 키로 검증합니다. 이 과정은 SSH 세션에서 사용자를 인증하는 것이지, 저장소의 소스 코드를 암호화하거나 커밋에 서명하는 것은 아닙니다.

JWT를 검증하는 서비스는 신뢰하는 발급자의 JSON 웹 키 집합(JSON Web Key Set, JWKS) 주소에서 공개 키를 가져옵니다. 인증서, 사전 등록, 키 지문(fingerprint), 신뢰할 수 있는 HTTPS 주소처럼 환경에 맞는 전달 경로가 있어야 디지털 서명의 의미가 완성되는 셈입니다.

대표적인 서명 알고리즘

알고리즘 이름을 보기 전에 계층부터 구분하면 덜 헷갈립니다. RSA와 타원 곡선 암호(Elliptic Curve Cryptography, ECC)는 넓은 기반 기술의 이름이고, RSA-PSS와 ECDSA는 그 위에서 서명하는 방식의 이름입니다. 또 다른 타원 곡선 서명 계열로 에드워즈 곡선 디지털 서명 알고리즘(Edwards-curve Digital Signature Algorithm, EdDSA)이 있는데요. Ed25519는 EdDSA의 곡선과 매개변수를 구체적으로 정한 알고리즘입니다.

알고리즘계열주로 눈여겨볼 점
RSA-PSSRSA기존 시스템과 호환하기 쉽지만 키와 서명이 큼
ECDSAECC작은 키를 사용하지만 서명마다 논스를 안전하게 다뤄야 함
Ed25519EdDSA작은 키와 서명, 서명할 때 별도의 난수가 필요하지 않음

RSA-PSS는 RSA를 사용하는 확률적 서명 방식입니다. RFC 8017은 새 애플리케이션에서 기존 RSASSA-PKCS1-v1_5보다 RSA-PSS를 사용하도록 요구하는데요. 같은 RSA 계열이어도 RSA-OAEP는 암호화 방식이고 RSA-PSS는 서명 방식이므로 서로 바꿔 쓸 수 없습니다. RSA는 오래된 시스템과의 호환성이 좋지만 같은 보안 강도의 타원 곡선 방식보다 키와 서명값이 큽니다.

타원 곡선 디지털 서명 알고리즘(Elliptic Curve Digital Signature Algorithm, ECDSA)은 더 작은 키로 강한 보안을 제공해 TLS와 JWT 등에서 널리 사용됩니다. 다만 서명마다 사용하는 임시 값인 논스(nonce)를 재사용하거나 예측할 수 있게 만들면 여러 서명으로부터 개인 키가 노출될 수 있습니다. 직접 난수를 만들거나 서명 공식을 구현하지 말고 검증된 라이브러리를 사용해야 하는 대표적인 이유입니다.

새 시스템이라고 해서 무조건 한 알고리즘만 정답인 것은 아닙니다. 상대 시스템의 지원 범위와 표준, 규제 요건, 기존 키 인프라를 함께 살펴야 합니다. 프로토콜이 지정한 매개변수까지 맞는지 확인해야 해요.

Ed25519는 어떤 알고리즘일까요?

EdDSA는 에드워즈 곡선을 사용하는 서명 방식의 계열 이름입니다. RFC 8032는 이 계열을 구체화한 Ed25519와 Ed448을 정의하는데요. Ed25519는 그중 edwards25519 곡선과 SHA-512를 사용하도록 매개변수를 정한 알고리즘입니다. 쉽게 말해 EdDSA가 설계 방식의 이름이라면 Ed25519는 프로그램이나 프로토콜에서 실제로 선택하는 구체적인 서명 알고리즘인 셈이죠.

Ed25519 개인 키로 메시지에 서명하면 Ed25519 공개 키로 그 서명을 검증합니다. 암호화나 복호화 기능은 없으며 두 참여자가 암호화 키를 합의하는 기능도 없습니다. Ed25519 공개 키는 32바이트, 서명값은 64바이트로 작습니다. 다만 ssh-keygen이 만드는 개인 키 파일에는 파일 형식과 메타데이터도 들어가므로 파일 크기가 곧 키의 크기를 뜻하지는 않습니다.

Ed25519는 서명마다 필요한 논스를 메시지와 개인 키에서 결정론적(deterministic)으로 계산합니다. 따라서 ECDSA에서 문제가 되는 논스 재사용이나 잘못된 난수에 대한 의존을 줄일 수 있죠. 물론 개인 키를 처음 생성할 때는 여전히 안전한 난수가 필요합니다.

이름이 비슷한 X25519는 키 합의(key agreement)에 사용합니다. 여기서 키를 합의한다는 것은 두 참여자가 서로 공개 값을 주고받은 뒤 자신들만 아는 같은 임시 비밀값(shared secret)을 각자 계산한다는 뜻입니다.

이름하는 일
Ed25519개인 키로 서명하고 공개 키로 검증
X25519자기 개인 키와 상대의 공개 값으로 같은 임시 비밀값을 계산

RFC 7748이 정의한 X25519로 계산한 임시 비밀값은 키 유도 함수(Key Derivation Function, KDF)를 거쳐 통신을 암호화할 대칭키의 재료가 됩니다. Ed25519와 X25519는 관련된 25519 곡선 계열을 사용해 이름이 닮았지만 목적과 연산, 키 형식이 다르므로 서로 바꿔 쓸 수 없습니다.

SSH용 Ed25519 키를 만들 때 실행하는 ssh-keygen -t ed25519도 이 서명 알고리즘의 키 쌍을 생성합니다. 접속할 때 클라이언트는 현재 SSH 세션에 묶인 인증 데이터에 Ed25519 개인 키로 서명하고 서버는 등록된 Ed25519 공개 키로 검증합니다. 소스 코드나 통신 데이터를 Ed25519로 암호화하는 것은 아닙니다.

TLS와 JWT, SSH, Git에서는 어떻게 쓰일까요?

TLS 1.3 핸드셰이크에서 서버는 인증서의 공개 키와 짝을 이루는 개인 키로 지금까지 주고받은 핸드셰이크 내용의 해시에 서명한 CertificateVerify를 보냅니다. 브라우저는 인증서의 공개 키로 이를 검증해 서버가 실제 개인 키를 가지고 있는지, 협상 내용이 중간에서 바뀌지 않았는지 확인합니다. 이후 HTTP 요청과 응답은 별도로 합의한 대칭키로 암호화되므로, 서명과 암호화가 각자 잘하는 일을 나눠 맡습니다.

JWS는 JSON 기반 데이터에 서명을 표현하는 표준입니다. JWT가 header.payload.signature 구조를 사용하는 것도 JWS의 한 응용인데요. 서명은 페이로드를 숨기지 않으므로 JWT를 디코딩해서 내용을 볼 수 있다는 사실과 위조할 수 있다는 말은 전혀 다릅니다. 내용을 고치면 서명 검증에 실패합니다.

SSH 공개 키 인증에서는 클라이언트가 세션에 묶인 데이터에 개인 키로 서명해 개인 키를 가지고 있음을 증명합니다. 개인 키 자체는 네트워크로 보내지 않습니다. 통신을 암호화할 대칭키는 X25519 같은 별도의 키 합의 방식으로 만든 키 재료에서 얻습니다.

Git은 커밋이나 태그 객체에 서명을 붙일 수 있습니다. 검증에 성공하면 해당 키로 서명되었고 객체가 서명된 뒤 바뀌지 않았음을 확인할 수 있는데요. 커밋 내용 자체는 그대로 보이며 그 키가 누구의 것인지는 GitHub 계정의 키 등록 정보나 로컬 신뢰 설정을 통해 별도로 판단합니다. SSH 로그인 키와 Git 커밋 서명에 모두 Ed25519 계열 키를 사용할 수 있지만 로그인 인증과 객체 서명은 서로 다른 작업입니다.

서명으로도 해결되지 않는 문제

디지털 서명은 강력하지만 유효한 서명 하나가 모든 신뢰 문제를 해결하지는 않습니다.

첫째, 개인 키가 유출되면 공격자도 정상적인 서명을 만들 수 있습니다. 개인 키는 비밀 관리 시스템이나 하드웨어 보안 모듈(Hardware Security Module, HSM)에 보관해야 합니다. 노출되었을 때 폐기하고 교체할 절차도 필요하죠.

둘째, 서명만으로 재전송 공격(replay attack)을 막을 수 없습니다. 공격자가 유효하게 서명된 송금 요청을 그대로 여러 번 보낼 수도 있는데요. 요청에 만료 시각과 한 번만 쓰는 값, 순번을 포함하고 서버가 사용 여부를 확인해야 합니다.

셋째, 무엇에 서명했는지가 명확해야 합니다. 눈에 보이는 JSON의 의미가 같아도 공백, 필드 순서, 문자 인코딩이 다르면 바이트열이 달라질 수 있습니다. 프로토콜이 정한 직렬화 방식과 서명 대상 바이트를 그대로 따라야 합니다. 서명한 데이터가 “로그인 승인”인지 “송금 승인”인지 구분할 수 있도록 문맥도 함께 묶어야 다른 용도의 서명을 가져다 쓰는 공격을 막을 수 있습니다.

마지막으로 유효한 서명은 개인 키가 사용되었다는 증거이지, 특정 사람이 내용을 읽고 자발적으로 동의했다는 사실까지 자동으로 증명하지는 않습니다. 키를 여러 사람이 공유했거나 악성 코드가 키를 사용했을 수도 있고, 서명 시각 이전에 키가 탈취되었을 수도 있습니다. 디지털 서명을 부인 방지(non-repudiation) 수단이라고 부르기도 하지만 실제 책임 판단에는 키 보관, 신원 확인, 감사 기록, 인증서 상태 같은 운영 증거가 함께 필요합니다.

마치며

디지털 서명은 데이터를 숨기는 암호화가 아닙니다. 개인 키를 가진 쪽이 메시지에 서명하고 공개 키를 가진 누구나 메시지의 무결성과 서명의 진위를 검증할 수 있게 하는 도구입니다. 해시는 메시지를 짧고 민감한 지문으로 만들고, 비대칭키 구조는 서명 권한과 검증 권한을 분리합니다. Ed25519는 이 서명 작업만 수행하는 EdDSA 계열의 알고리즘이며, 두 참여자가 같은 임시 비밀값을 계산하게 하는 X25519와는 역할이 다릅니다.

다만 검증 성공은 출발점일 뿐입니다. 공개 키의 주인을 신뢰할 수 있어야 하고 개인 키도 안전하게 관리해야 합니다. 재전송 방지와 정확한 직렬화 같은 프로토콜 설계도 뒤따라야 하죠. 그래야 TLS의 서버 인증, JWT 검증, SSH 로그인, Git 커밋 서명을 믿을 수 있습니다.

표준에서 정의하는 알고리즘과 요구 사항을 더 살펴보려면 NIST FIPS 186-5 디지털 서명 표준을 참고하세요.

This work is licensed under CC BY 4.0CCBY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord