[연재] 클라이언트 싸이닝과 이를 응용한 웹로그인

클라이언트 싸이닝 과 이를 응용한 웹로그인


클라이언트 싸이닝이란 말 그대로 서버에서가 아니라, 클라이언트쪽에서  싸인하는 것을 말한다.

이는 우리 실재 생활과도 유사한데, 계약서 등 우리는 자기가 작성한 문서 마지막에 '이 문서는 내가 작성한 것이 맞습니다' 라는 의미의 싸인, 기명날인을 한다.

                                                                 Copyright Pixabay

그럼 이걸로 어떻게 웹로그인을 한다는 것일까? 복잡한 구현 방식이 있겠지만 여기서는 아이디어 차원에서 최대한 단순하게 다뤄보도록 하겠다.


원리는 간단하다.

1. 특정 문자열(사용자 세션아이디 + 타임스탬프)을 서버가 문자열로 클라이언트에게 던진다.

2. 그 문자열을 받은 클라이언트는 자신의 개인키로 문자열을 싸인해서 최초 문자열과 함께 서버로 보낸다.

3. 서버는 그 문서를 열고 해당 문서가 누가 보냈는지 확인하고, 해당 세션을 로그인해준다.


장점

위 방식의 최대 장점은 싸인을 클라이언트에서 하기 때문에 서버에 클라이언트 비밀번호가 없다는 것이다.

블럭체인 쪽에서는 당연한 것이지만, 일반 웹 로그인에서는 엄청 중요한 의미를 가진다.


자 그러면 이를 구현해보자.

구현은 노드로 작성하겠으며, web3.js를 이용할 것이다.

사용할 web3.js는 현재 가장 최신 버전인 '1.0.0-beta.36'이다.


클라이언트 sign

wallet이나 JSON V3 Keystore에서 개인키(private key)를 가져오는 방법에 대해서는 다루지 않겠다.

일단 개인키를 가져왔다고 하고, 바로 코딩하자.

일단 서버는 이미 가입한 사용자 목록을 가지고 있고, 나중에 싸인한 사람을 그 목록에서 찾아 로그인 처리할 것이다.

서버가 보내 줄 데이터는 서버로 로그인하려는 클라이언트의 웹 세션정보와 로그인 시간이다.

var challenge = { sid: 'session id', timestamp: '1544583497826' };


자 이 데이터를 싸이닝하기 위해서는 먼저 JSON 데이터이므로 String으로 변환하고, web3.utils.toHex를 사용해서 이를 Hex 값으로 바꿔야 한다.

이제 web3.eth.accounts.sign를 사용해서 해당 문자열을 싸인해보자.


const Web3 = require('web3');
const web3 = new Web3();

var challenge = { sid: 'session id', timestamp: '1544583497826' };
var data_from_server = JSON.stringify(challenge);
var private_key = '0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318';
var signed_data = web3.eth.accounts.sign(web3.utils.toHex(data_from_server), private_key);
console.log('Signed data', signed_data);


위 코드를 실행해서 싸인된 값을 출력해 보면 RLP 인코딩된 시그너쳐가 포함된 다음의 JSON 결과가 나올 것이다.

Signed data { message: '0x7b22736964223a2273657373696f6e206964222c2274696d657374616d70223a2231353434353833343937383236227d',
  messageHash: '0x659009bf24d4843660da9853d55783d89705b74518389c84e9a7f54ccf5c7371',
  v: '0x1b',
  r: '0x3ff390d434176cc6ac52fc1b4bd62e280964e82d02fcfb55f7284f5ed0978b55',
  s: '0x2396e5466983eb33f73a2eb82d6cf723c4aa8189e5c98685dd915a30e3ed4be6',
  signature: '0x3ff390d434176cc6ac52fc1b4bd62e280964e82d02fcfb55f7284f5ed0978b552396e5466983eb33f73a2eb82d6cf723c4aa8189e5c98685dd915a30e3ed4be61b' }

RLP란 Recursive Length Prefix 의 약자로 이더리움 내부에서 문자열이나, 배열 등을 인코딩하는 패키지이다. RLP를 사용하는 목적은 복잡한 형식의 데이터를 하나의 규격화된 형식으로 직렬화하여 전송하기 위한 것이다.

자바 프로그램을 해보신 분들은 자바의 직렬화 인터페이스(java.io.Serializable)라고 생각하면 된다.

형식은 다음과 같다.

        message - String: Hex로 인코딩한 문자열.
        messageHash - String: message를 sha3로 인코딩한 값.
        r - String: signature 의 처음
        s - String: signature 의 처음 32바이트
        v - String: Recovery 값 + 27
signature - r + s + v

여기서 중요한 것은 RLP로 인코딩된 signature 값이다 . 이 값은 r + s + v 값의 연결이다.

이 값을 서버로 전송하면 된다. 전송 방법은 프로그램에 따라 다를 수 있으므로 여기서는 생략하도록하자

다음과 같은 의사코드가 가능하다.

send_to_server(message,signature);


서버 recover

클라이언트로 부터 받은 signature를 가지고 누가 싸인해서 보냈는지 확인해보자.

서버가 클라이언트로부터 받는 데이터는 위 의사코드 파라미터  2개다.

  1. 최초 자신이 보냈던 메세지
  2. 이를 싸인한 signature

자 이 둘을 가지고 web3.eth.accounts.recover 함수를 사용하여 싸인한 사람의 주소를 알아내보자.

const Web3 = require('web3');
const web3 = new Web3();

var message = '{"sid":"session id","timestamp":"1544583497826"}';
var signature = '0x3ff390d434176cc6ac52fc1b4bd62e280964e82d02fcfb55f7284f5ed0978b552396e5466983eb33f73a2eb82d6cf723c4aa8189e5c98685dd915a30e3ed4be61b';
var signer = web3.eth.accounts.recover(message,signature);
console.log('Signer',signer);

위 코드를 실행하면 아래와 같이 싸인한 사람의 주소가 나온다.

Signer 0x2c7536E3605D9C16a7a3D7b1898e529396a65c23

남은 것은 해당 세션에 위 주소를 가진 사용자를 로그인해주면 된다. 물론 타임스탬프를 확인하여 정해진 시간안에 로그인이 이루어졌는지 확인해야할 것이다.


유의점

클라이언트에서 메세지를 sha3으로 인코딩할 때 이더리움은 SHA-3 FIPS 202 표준을 사용하는 것이 아니라  Keccak-256 를 사용하고 입력 메세지를 다음과 같이 바꾼다.

"\x19Ethereum Signed Message:\n" + message.length + message

이 사항에 대해서는 여기를 참조하기 바란다.


결론

여기서 우리는 이더리움 sign 기술을 사용하여 이를 웹 사용자 로그인에 응용하였다.

만약 사용하는 클라이언트가 안드로이드 등 자바 베이스라고 한다면 web3j 모듈을 사용해야 할 것이다.

web3j 버전이 4.0.0 이전 버전일 경우 sha3 함수를 사용하여 인코딩할 때 ""\x19Ethereum Signed Message:\n" + message.length + message" 이 부분 코딩해 넣어야 할 것이다.

이슈를 참조하기 바란다.