ERC-1271 컨트랙트 서명 검증 표준 (Standard Signature Validation Method for Contracts)
Final · 서명·인증 · 2018-07-25 작성
개인 키가 없는 스마트 컨트랙트도 자신을 대신한 서명이 유효한지 알려 줄 수 있게 합니다. 서명 컨트랙트가 isValidSignature 함수를 구현하면 어떤 앱이든 같은 방식으로 서명을 검증합니다.
왜 필요한가
이더리움의 일반 계정(EOA, Externally Owned Account)은 개인 키로 메시지에 서명할 수 있습니다. 그러나 스마트 컨트랙트에는 개인 키가 없어서 같은 방식의 서명을 만들 수 없습니다. 한편 서명된 메시지로 자산을 옮길 권한을 확인하는 컨트랙트는 지금도 많고 앞으로도 늘어납니다. 예를 들어 주문장을 체인 밖에서 관리하는 탈중앙 거래소는 매수·매도 주문을 서명된 메시지로 받습니다. 사용자는 서명으로 거래 의사를 밝히고 거래소 컨트랙트가 거래를 마무리할 권한을 줍니다. 컨트랙트 지갑이나 DAO, 다중 서명 지갑 같은 컨트랙트 계정은 이런 앱을 쓸 수 없었습니다. 어떤 서명이 자신을 대신한 유효한 서명인지 컨트랙트가 알려 줄 공통 방법이 없었기 때문입니다. 이 표준은 그 방법을 함수 하나로 정합니다.
구현해야 하는 것
bytes4 constant internal MAGICVALUE = 0x1626ba7e;: 검증에 성공했을 때 돌려주는 매직값입니다. isValidSignature(bytes32,bytes) 시그니처의 keccak256 해시 앞 4바이트입니다.
심층 설명
함수 하나와 매직값 하나
표준은 isValidSignature(bytes32 _hash, bytes memory _signature) 함수 하나만 요구합니다. _hash는 서명 대상 데이터의 해시, _signature는 그 해시에 대한 서명 바이트 배열입니다. 반환형은 bytes4이고 검증을 통과하면 반드시(MUST) 매직값 0x1626ba7e를 돌려줘야 합니다. 이 값은 bytes4(keccak256("isValidSignature(bytes32,bytes)"))입니다. 불리언 대신 특정 값을 돌려주게 한 이유는 서명 검증을 더 엄격하고 단순하게 하기 위해서입니다. 호출 측은 반환값이 0x1626ba7e와 같은지만 비교하면 됩니다. 참고 구현은 실패 시 0xffffffff를 돌려주지만 표준은 성공 시 값만 정합니다.
상태를 바꾸지 않고 외부에서 호출할 수 있어야 합니다
isValidSignature는 상태를 변경하면 안 됩니다(MUST NOT). solc 0.5 미만에서는 STATICCALL로 호출하고 0.5 이상에서는 view 한정자를 붙입니다. 또한 외부 호출을 허용해야 합니다(MUST allow external calls). 상태 변경을 막는 이유는 GasToken 발행 같은 공격 경로를 차단하기 위해서입니다. 구현 범위를 단순하게 해 표준화를 쉽게 하고 체인 밖에서도 컨트랙트에 질의할 수 있게 하는 목적도 있습니다.
검증 규칙은 컨트랙트가 정합니다
isValidSignature 안에서는 임의의 메서드를 호출해 서명을 검증할 수 있습니다. 원문은 세 갈래 예를 듭니다. 시간이나 상태에 따라 달라지는 맥락 의존 검증, 스마트 지갑 안에서 서명자의 권한 수준을 보는 EOA 의존 검증, ECDSA·멀티시그·BLS 같은 서명 방식 의존 검증입니다. 이 함수는 메시지에 서명하려는 컨트랙트, 즉 스마트 컨트랙트 지갑, DAO, 다중 서명 지갑 등이 구현합니다. 컨트랙트 서명을 지원하려는 앱은 서명자가 컨트랙트일 때 이 함수를 호출합니다. 함수 이름에 '유효한(valid)'을 쓴 이유는 권한 있는 서명자가 만든 서명만 서명 컨트랙트가 유효하다고 보기 때문입니다. 서명된 행동 메시지는 그 서명자가 지갑을 대신해 해당 행동을 할 권한이 있을 때만 유효합니다.
왜 메시지가 아니라 해시를 받는가
서명 대상과 서명을 단순하게 분리하려고 인자를 해시와 서명 둘로 나눴습니다. 원본 메시지 대신 bytes32 해시를 받는 이유는 컨트랙트가 EIP-712처럼 표준이 아닌 특정 해시 함수를 기대할 수 있기 때문입니다. 표준은 해시를 만드는 방식을 지정하지 않습니다. 따라서 앱은 서명 컨트랙트가 기대하는 방식으로 해시를 만들어 넘겨야 합니다. 어떤 해시 방식을 쓰든 함수 시그니처는 bytes32 하나로 같습니다.
참고 구현: 소유자 서명 복구
원문의 참고 구현은 서명자가 컨트랙트의 owner인지 확인합니다. recoverSigner(_hash, _signature)로 서명자를 복구해 owner와 같으면 0x1626ba7e, 다르면 0xffffffff를 돌려줍니다. recoverSigner는 EthSign 서명 전용이고 서명은 (bytes32 r, bytes32 s, uint8 v) 순서로 65바이트여야 합니다. 서명 가변성(malleability)을 막기 위해 s가 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0보다 크면 되돌리고 v는 27 또는 28만 허용합니다. 라이브러리가 v를 0/1로 만들면 27을 더해 받아들일 수 있다고 주석에 적혀 있습니다. ecrecover 결과가 0x0 주소면 실패 처리합니다. 이 검사들은 OpenZeppelin의 ECDSA 라이브러리를 출처로 밝힙니다.
호출 측 구현과 호환성
서명 컨트랙트를 호출하는 쪽의 예시도 있습니다. callERC1271isValidSignature(address _addr, bytes32 _hash, bytes calldata _signature)는 IERC1271Wallet(_addr).isValidSignature(_hash, _signature)를 호출하고 결과가 0x1626ba7e와 같지 않으면 INVALID_SIGNATURE로 되돌립니다. 반환값은 불리언으로 해석하지 않고 매직값과 정확히 비교합니다. 이 표준은 컨트랙트 서명에만 해당하고 EOA 서명은 다루지 않으므로, 기존 서명 검증 방식과 충돌하지 않고 함께 쓸 수 있습니다.
쓸 때 주의할 점
- 호출 가스 양을 고정하면 안 됩니다: isValidSignature 호출에는 정해진 가스 한도가 없어서 어떤 구현은 많은 가스를 쓸 수 있습니다. 외부 컨트랙트의 이 함수를 호출할 때 보내는 가스 양을 하드코딩하면 특정 서명은 검증되지 못할 수 있습니다.
- 검증 책임은 구현 컨트랙트에 있습니다: 이 함수를 구현하는 각 컨트랙트는 전달된 서명이 실제로 유효한지 스스로 보장해야 합니다. 그렇지 않으면 치명적인 결과가 생길 수 있습니다.