하루살이 개발일지

생체인증을 통한 전자서명 (RSA 암호화와 SHA-256 해싱 알고리즘) 본문

앱개발/React-Native

생체인증을 통한 전자서명 (RSA 암호화와 SHA-256 해싱 알고리즘)

harusari 2024. 12. 29. 17:54

 

 

현대 디지털 세계에서는 데이터 보안이 무엇보다 중요하다. 서버는 민감한 정보를 클라이언트에 무분별하게 제공할 수 없기 때문에, 정보를 검증하거나 안전하게 전달하기 위해 디지털 서명 방식이 필수적이다. 이를 위해 널리 사용되는 두 가지 핵심 기술이 바로 RSA 암호화SHA-256 해싱 알고리즘이다.

 

프로젝트 당시 재화가 오고가는 기능을 구현하는 과정 중 본인인증이 필요한 요구사항이 있었다. 앱플로우 상 꽤나 빈번하게 발생할 검증 과정이었기 때문에, UX를 위해 PASS인증이나 카카오톡 인증 방식 등을 사용하기에는 번거로울 거라고 판단하였다. 따라서 생체인증을 활용한 전자서명 방식을 채택하였고, 이를 구현하는 과정에서 RSA와 SHA-256에 대한 개념을 접했다.

 

RSA와 SHA-256이 보안과 무결성을 보장하는 데 어떤 역할을 하는지 구체적으로 설명하고자 한다. 생체인증뿐만 아니라 블록체인, 디지털 서명과 같은 다양한 보안 시스템에서도 이 기술들은 핵심 요소로 사용된다고 한다. 이 글에서는 특히 RSA에 중점을 두고 그 작동 원리를 분석해 보고자 한다.

 


1. RSA: 비대칭키 암호화 알고리즘

RSA는 비대칭키 암호화를 기반으로 한 알고리즘으로, 공개키와 개인키(=비밀키)를 이용해 데이터를 암호화하고 복호화한다. 이 과정에서 SHA-256 같은 해싱 알고리즘이 함께 사용되며, 데이터의 무결성을 검증한다.

 

 

RSA의 작동 원리

출처 : https://science.snu.ac.kr/newsroom/view/2/11/992

 

RSA는 크게 다음과 같은 과정으로 작동한다:

 

1. 키 생성

  • 두 개의 큰 소수를 기반으로 공개키와 개인키를 생성한다.
  • 공개키는 누구나 볼 수 있으며 데이터를 암호화하는 데 사용된다.
  • 개인키는 비밀로 유지되며 암호화된 데이터를 복호화하는 데 사용된다.

 

2. 암호화

  • 평문(보내고자 하는 메시지)을 공개키를 사용하여 암호화한다.
  • 암호화된 데이터(=서명)는 수신자에게 전송된다.

3. 복호화

  • 수신자는 개인키를 사용하여 암호화된 데이터를 복호화한다.
  • 복호화를 통해 원래의 메시지(평문)를 복원할 수 있다.

 

RSA와 챌린지 데이터 검증

서버에서 클라이언트의 데이터를 검증할 때, 챌린지 데이터가 추가로 활용될 수 있다. 이 과정은 다음과 같다:

  1. 서버가 클라이언트에게 고유한 챌린지 데이터를 생성하여 전송한다.
  2. 클라이언트는 이 챌린지 데이터를 포함해 메시지를 구성한 후, SHA-256으로 해싱하여 해시 값을 생성한다.
  3. 클라이언트는 해시 값을 RSA 개인키로 암호화해 디지털 서명을 생성하고 서버로 전송한다.
  4. 서버는 공개키로 서명을 복호화하고, 챌린지 데이터를 포함한 원본 메시지를 해싱하여 결과를 비교한다.
  5. 두 해시 값이 일치하면 검증에 성공한다.

예시: 사용자가 "LoginRequest"라는 메시지와 함께 챌린지 데이터 "12345"를 받았다면, 이 두 데이터를 결합해 SHA-256 해싱을 수행한다. 클라이언트는 RSA 개인키로 서명을 생성해 전송하며, 서버는 공개키를 이용해 이를 확인하고 무결성을 검증한다.

 

 

RSA의 활용 사례

  • 전자 서명: 메시지가 변조되지 않았음을 증명.
  • SSL/TLS: 안전한 웹 통신 보장.
  • 보안 데이터 전송: 민감한 데이터를 안전하게 공유.

 


2. SHA-256: 데이터의 무결성을 보장하는 해싱 알고리즘

SHA-256은 데이터를 고정된 크기의 고유한 해시 값으로 변환하는 알고리즘이다. 이 과정에서 입력 데이터의 무결성을 보장하며, RSA와 결합해 디지털 서명 생성에 사용된다.

 

 

SHA-256의 작동 원리

  1. 데이터 분할: 입력 데이터를 512비트 블록 단위로 분할한다.
  2. 초기 해시 값 설정: 고정된 256비트 초기 해시 값을 설정한다.
  3. 압축 함수 반복: 각 블록을 처리하며 복잡한 비트 연산과 압축 함수를 반복 적용한다.
  4. 최종 출력: 고유한 256비트 해시 값이 생성된다.

예시: "Hello, World!"라는 메시지를 입력하면, SHA-256은 다음과 같은 고유한 해시 값을 출력한다:

a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b5e6cce00bf9f52d1

만약 메시지의 한 글자라도 변경된다면, 완전히 다른 해시 값이 생성된다.

 

SHA-256의 특징

  • 단방향성: 해시 값에서 원본 데이터를 역추적할 수 없다.
  • 변경 감지: 입력 데이터가 조금이라도 바뀌면 완전히 다른 해시 값을 생성한다.
  • 충돌 회피: 동일한 해시 값을 가지는 서로 다른 입력 데이터를 찾기 매우 어렵다.

 

SHA-256의 활용 사례

  • 블록체인: 데이터 블록 간 무결성을 유지.
  • 비밀번호 저장: 비밀번호를 직접 저장하지 않고 해시 값으로 변환.
  • 디지털 서명: 메시지의 무결성을 증명.

 


3. 생체인증과 전자서명에서의 활용

생체인증은 RSA와 SHA-256을 활용하여 더 높은 수준의 보안을 제공한다. 사용자의 고유 생체 정보를 활용해 데이터 무결성을 보장하고, 인증 과정을 간소화한다. 여기에 챌린지 데이터를 포함함으로써 클라이언트-서버 간 통신의 신뢰성을 더욱 강화할 수 있다.

생체인증 기반 전자서명의 흐름

  1. 사용자의 지문이나 얼굴 데이터를 입력받는다.
  2. 챌린지 데이터와 결합하여 SHA-256으로 해싱하여 고유한 해시 값을 생성한다.
  3. RSA 개인키로 이 해시 값을 암호화해 디지털 서명을 생성한다.
  4. 서버는 챌린지 데이터를 포함한 원본 데이터를 해싱하고, RSA 공개키로 서명을 검증하여 데이터의 무결성을 확인한다.

예시: 사용자가 모바일 앱에서 로그인할 때 지문을 스캔하고 서버에서 받은 챌린지 데이터를 조합한다. 이 데이터를 해싱한 후 RSA 개인키로 암호화하여 전송하면, 서버는 공개키를 통해 서명을 검증하고 사용자 인증을 완료한다.

 


4. 구현 방식

아래 코드는 생체인증 라이브러리인 react-native-biometrics 를 이용해 생체인증을 통한 전자서명 방식을 구현한 커스텀 훅이다.

import ReactNativeBiometrics, { BiometryTypes } from "react-native-biometrics";
import { Toast } from "react-native-toast-notifications";

export const useBiometricsAuthentication = () => {
  const rnBiometrics = new ReactNativeBiometrics({
    allowDeviceCredentials: true,
  });

  //생체인증 가능한지 확인하는 함수
  const checkIsBiometricsAvailable = async () => {
    const result = await rnBiometrics.isSensorAvailable();

    if (!result) {
      return { type: BiometryTypes.Biometrics, isAvailable: false };
    }

    const { biometryType } = result;
    const isAvailable = [
      BiometryTypes.FaceID,
      BiometryTypes.TouchID,
      BiometryTypes.Biometrics,
    ].includes(biometryType || "");

    return {
      type: biometryType || BiometryTypes.Biometrics,
      isAvailable,
    };
  };

  //서명 없이 생체인증하고 싶을때
  const authenticateWithoutSignature = async () => {
    const result = await rnBiometrics.simplePrompt({ //생체인증 트리거
      promptMessage: "생체인증을 진행해주세요.",
    });

    return { success: result?.success || false };
  };

  //createKeys()를 사용하면 RSA로 키를 만들어줌
  const createKeyPair = async () => {
    const result = await rnBiometrics.createKeys();

    if (result?.publicKey) {
      return { success: true, publicKey: result.publicKey };
    }

    return { success: false, publicKey: null }; //public key만 알수있고 비밀키는 안전한 곳에 저장됨
  };

  const handleKeyPairCreation = async () => {
    const authSuccess = await authenticateWithoutSignature();
    if (authSuccess.success) {
      const keyPair = await createKeyPair();
      if (keyPair.success) {
        Toast.show({ type: "success", text: "키페어 생성 성공" });

        console.log("키페어 생성 성공, 이후 인증 시작");
        await handleSignatureCreation();
      } else {
        Toast.show({ type: "error", text: "키페어 발급 실패" });
      }
    } else {
      Toast.show({ type: "error", text: "생체 인증 실패" });
    }
  };

  //키페어 삭제 함수
  const resetKeystore = async () => {
    const keysExist = await rnBiometrics.biometricKeysExist();

    if (keysExist?.keysExist) {
      const keysDeleted = await rnBiometrics.deleteKeys();

      if (keysDeleted?.keysDeleted) {
        Toast.show({ type: "success", text: "키페어 삭제 성공" });
      } else {
        Toast.show({ type: "error", text: "키페어 삭제 실패" });
      }
    }
  };

 
  const handleSignatureCreation = async () => {
    const signatureResult = await rnBiometrics.createSignature({
      promptMessage: "로그인을 위해 생체인증을 진행해주세요.",
      payload: `myPayload`, //이곳에 보통 챌린지데이터를 넣음
    });

    if (signatureResult?.success) {  
      Toast.show({ type: "success", text: "서명 생성 성공." });
    } else {
      Toast.show({ type: "error", text: "서명 생성 실패." });
    }
  };
  
   //생체인증 할 곳에서 호출할 함수
   const checkBiometricAuthentication = async () => {
    const biometrics = await checkIsBiometricsAvailable();
    if (!biometrics.isAvailable) {
      Toast.show({ type: "error", text: "생체인증 사용 불가능" });
      return;
    }

    const keysExist = await rnBiometrics.biometricKeysExist();

    if (!keysExist?.keysExist) {
      await handleKeyPairCreation(); //없으면 키 생성
    } else {
      await handleSignatureCreation(); //있으면 서명 생성
    }
  };


  return {
    checkBiometricAuthentication,
    resetKeystore,
    createKeyPair,
  };
};

 

 

💭 생체인증 말고 간편비밀번호 등을 활용하는 방법은 없을까? 더보기...

더보기

React-native-biometrics 라이브러리에서 제공해주는 메소드(키페어 생성, 서명 생성)를 직접 구현하면 생체인증 이외의 수단으로도 전자서명을 구현할 수 있다고 생각했다. 전자서명을 생성하는 과정은 굵게 다음과 같은 과정이 포함되는데,

 

1. 키 생성

2. 암호화를 통한 서명 생성

3. 서명 검증 (이 과정은 서버가 담당)

 

1, 2를 구현하기 위해 react-native-quick-crypto 라이브러리를 활용하였다.

이 라이브러리는 RSA와 SHA-256 알고리즘을 구현해 놓은 구현체이다. 

 

코드로 구현하면 다음과 같다.

 

1. 키 생성

import crypto from "react-native-quick-crypto";

//키 생성하는 함수
const generateRSAKeyPair = () => {
    const keypair = crypto.generateKeyPairSync("rsa", {
      modulusLength: 2048,
      publicKeyEncoding: { type: "spki", format: "pem" },
      privateKeyEncoding: { type: "pkcs8", format: "pem" },
    });

    return keypair;
  };

 

2. 암호화를 통한 서명 생성

  // 데이터 서명 함수
  const signData = (data: Buffer, privateKey: string) => {
    try {
      // SHA256 알고리즘으로 Sign 객체 생성
      const sign = crypto.createSign("SHA256");
      sign.update(data); // 서명에 포함할 데이터 업데이트

      // 비밀키로 서명 생성
      const signature = sign.sign(
        {
          key: privateKey,
          format: "pem", // 비밀키 형식
          type: "pkcs8", // 비밀키 타입
        },
        "base64"
      ); // 결과를 Base64로 반환

      return signature;
    } catch (error) {
      return null;
    }
  };

 

 

이렇게 하면 어떤 수단으로도 데이터 전자서명 방식을 구현할 수 있게 된다!

 

 

 


 

5. 결론

 

RSA와 SHA-256은 현대 보안 기술에서 중요한 역할을 한다. RSA는 데이터를 안전하게 암호화하고 복호화하며, SHA-256은 데이터의 무결성을 보장한다. 이 두 기술은 생체인증 시스템과 결합하여 강력한 보안성과 편의성을 제공한다.

생체인증 기반 전자서명은 다음과 같은 장점을 제공한다:

  • 보안 강화: 생체 정보는 복제하기 어렵다.
  • 데이터 무결성 보장: SHA-256을 사용해 데이터 변조를 방지한다.
  • 편의성 제공: 비밀번호 대신 생체 데이터를 사용하여 인증한다.
  • 신뢰성 향상: 챌린지 데이터를 포함하여 통신 과정의 신뢰도를 보장한다.