Trusted Signer

Vela는 승인하기 전에 모든 거래를 디코딩하고, 그 디코딩은 성실하게 이루어집니다. 하지만 그 작업을 하는 것은 거래를 만든 바로 그 앱입니다. 앱이나 앱이 사용자에게 전달되는 경로가 변조되면, 한 가지를 보여 주고 다른 것에 서명하게 할 수 있습니다. 바이비트에서 일어난 일이 정확히 그것입니다.

Trusted Signer는 이것을 둘로 나누기 위해 있습니다. 앱은 요청을 넘겨주기만 하고, 확인과 서명은 별도의 페이지에서 이루어집니다. 처음부터 끝까지 읽을 수 있고, 바이트 하나까지 똑같이 다시 빌드할 수 있고, 직접 운영할 수도 있는 페이지입니다.

어디에서 실행되나

공식 페이지는 sign.getvela.app에서 서비스됩니다. 데스크톱(macOS, Windows, Linux), iPhone, Android 앱은 이 페이지로 요청을 보낼 수 있습니다. 앱이 요청을 링크에 담아 브라우저 탭에서 페이지를 열면, 사용자는 그곳에서 요청을 확인하고 패스키로 서명합니다. 그러면 페이지가 velawallet:// 링크로 서명을 앱에 돌려줍니다. 웹 지갑에서는 사용할 수 없습니다.

직접 선택해야 쓰는 기능입니다. 지갑을 만들거나 로그인할 때 서명 방식으로 이것을 고르면, 그때부터 그 기기에서 그 지갑의 모든 서명이 이 페이지를 거칩니다. 지갑의 키를 만들 수도 있습니다. sign.getvela.app에서는 앱과 같은 getvela.app 패스키를 사용합니다.

서명하기 전에 하는 일

  • 요청을 스스로 디코딩합니다. 호출이 무엇을, 누구에게, 얼마나 하는지 calldata에서 읽어 냅니다. 묶음 안에 들어 있는 호출도 포함합니다.
  • 직접 계산한 다이제스트에만 서명합니다. EIP-191, EIP-712, SafeOp, SafeMessage 다이제스트는 페이지 안에서 계산하며, 요청하는 쪽에서 받아 쓰는 일은 없습니다. SafeOp와 SafeMessage 다이제스트는 지갑이 쓰는 코드인 vela-core와 테스트로 대조하고, 앱은 자신이 직접 계산한 것이 아닌 다이제스트에 대한 서명은 거부합니다.
  • 요청받은 거래가 맞는지 확인합니다. 사이트가 요청한 호출이 서명할 오퍼레이션 안에 실제로 들어 있어야 하며, 그렇지 않으면 페이지가 거부합니다.
  • 승인이 무제한이면 그렇다고 알려 줍니다. 이 페이지는 금액을 바꿀 수 없습니다. 도착한 바이트에 서명하거나 아무것도 서명하지 않을 뿐입니다. 그래서 무제한 승인이나 permit(2^200 이상, Permit2는 2^152 이상 — 앱과 같은 기준)은 그 이유와 함께 빨간색으로 표시되고, 있는 그대로 서명할 수 있습니다. 온체인 한도는 요청이 여기 오기 전에 지갑 자체의 승인 화면에서 고릅니다. NFT 컬렉션 전체에 대한 승인은 거부합니다.
  • 책임질 수 없는 것은 거부합니다. eth_sign, 모르는 메서드, 토큰을 그 토큰 자신의 컨트랙트로 보내는 것, 읽을 수 없는 오퍼레이션, 그리고 요청하는 쪽이 챌린지를 정한 로그인입니다.
  • 계정을 넘겨주게 되는 것은 거부합니다. 앱과 같은 규칙으로, 계정이 자기 자신의 소유자·모듈·가드·폴백 함수를 호출하는 것(배치 안에서도), Safe의 MultiSend 컨트랙트(한 오퍼레이션의 호출들을 묶는 것)로 가는 것을 제외한 delegatecall, 그리고 SafeTx 서명입니다. 사이트가 요청한 호출만이 아니라, 앱이 조립한 오퍼레이션의 모든 호출을 확인합니다.
  • 계정 주소와, 페이지에서 계산한 아이덴티콘을 보여 줍니다. 받는 사람이나 컨트랙트의 이름을 요청에서 가져와 붙이지 않습니다. 컨트랙트에 이름을 붙일 수 있는 것은 페이지 자체의 검토된 목록뿐입니다. 계정 자체의 이름은 알맞은 패스키를 고를 수 있도록 앱이 보내 주는 것으로, 주소 옆에 표시됩니다.
  • 서명할 때마다 사용자 인증(지문, 얼굴 또는 PIN)을 요청합니다.

일부러 넣지 않은 것

  • 편집 기능이 없습니다. 요청은 도착한 순간 고정됩니다. 서명하거나, 하지 않거나 둘 중 하나입니다. 수수료 선택이나 한도 편집 기능은 calldata를 다시 쓰게 되는데, 그것이야말로 이 페이지가 막으려는 병입니다.
  • 네트워크에 접근하지 않습니다. 이 페이지는 파일 하나이고, 그 콘텐츠 보안 정책 (default-src 'none')이 파일 자체의 바이트 안에 들어 있습니다. 그래서 무엇을 가져오거나, 연결을 열거나, 이미지를 불러올 수 없습니다. 페이지 밖으로 나가는 것은 응답뿐이며, 그것도 요청에 담긴 콜백 링크(Vela 앱이 요청했다면 velawallet://)를 따라갈 때뿐입니다. 토큰 로고는 글자로 그립니다.

앱도 다시 확인합니다

앱도 이 페이지를 믿지 않습니다. 서명된 챌린지가 앱이 계산한 다이제스트이고, 사용자 인증을 거쳤고, 키가 내 지갑의 키 중 하나이며, P-256 서명이 그 키로 검증될 때만 서명을 받아들입니다.

공개된 모든 버전을 검증할 수 있습니다

각 버전은 app-web/trusted-signer/src/에서 파일 하나로 재현 가능하게 빌드되고(Bun과 Node가 같은 바이트를 만들어 냅니다), 이전의 모든 버전과 나란히 자기만의 주소인 sign.getvela.app/b/<sha256>/sign.html에 공개됩니다. 목록은 sign.getvela.app/index.json에 있습니다.

cd app-web/trusted-signer
node samples/build-single.mjs --check   # rebuilds a version listed in dist/
curl -sL https://sign.getvela.app/b/<sha256>/sign.html | shasum -a 256

데스크톱 앱은 시작할 때, 열게 될 공개 버전을 받아 해시를 계산하고 앱에 내장된 버전들과 비교합니다. 그 결과는 로그에만 남고, 일치하지 않는 페이지도 그대로 열립니다. 휴대폰 앱은 아직 이 확인을 하지 않습니다.

내 사본 운영하기

설정에는 앱이 여는 페이지의 주소가 저장되어 있어서, 이를 내 배포로 바꿔 지정할 수 있습니다. 어떤 HTTPS 주소든 되고, 테스트용으로는 localhost도 됩니다. bun samples/build-single.mjs(또는 node)로 빌드한 다음 dist/를 내 호스트에 복사하세요.

내 도메인에 둔 사본은 getvela.app 패스키가 아니라 그 도메인용으로 만든 패스키로 서명합니다. 따라서 이것은 키가 내 도메인 아래에 있는 지갑을 만들고 쓰는 방법이지, 기존 getvela.app 지갑에 서명하는 방법이 아닙니다. 한 지갑의 키는 모두 하나의 도메인을 공유합니다.

코드와, 그것을 빌드하고 검증하는 스크립트는 app-web/trusted-signer/에 있습니다.