はじめに
nem Advent Calendar 2021の13日目ですね。
近畿大学でブロックチェーンや秘密鍵周りのことを学んでいるいなたつです。
Symbolを使ったりしてるどこかの企業でインターンをしたりしてます。(隠す意図は特にない)
githubとかポートフォリオ見ればたぶんわかりますし。
Symbolを使ってブロックチェーンでなんやかんやする研究とかをしてます。
今回はそのなんやかんやに関する記事です。
早速本題(問題定義)
秘密鍵をWebアプリケーション上で扱うことは怖くないですか?
あなたは、何を、誰を信用してWebアプリケーション上で署名をしていますか?
入力した秘密鍵は提示された用途以外で使用される可能性が無いと言えますか?
秘密鍵を盗られるということ
秘密鍵が何者かに盗まれると、所有者の意志を介さずにトランザクションへと署名することが可能になってしまいます。なりすましですね。
全ての資産が盗まれます。
Webアプリケーション上でトランザクションに署名するリスク
ユーザーがSymbolブロックチェーンを直接活用するWebアプリケーションを作成する際秘密鍵を扱う必要性があります。
秘密鍵を直接入力し署名をする場合や、Webアプリケーション運営者は暗号化した秘密鍵をlocalStorage等に保存し、ユーザはパスフレーズを管理しその二つを用いて秘密鍵を導出し署名を行う等の方法があります。
しかし、これらはどちらの場合でも、善意の開発者の元行われるという前提で成り立っています。信頼するしかありません。
悪意のあるWebアプリケーション
まずはWebアプリケーションの作成者が悪意を持ってアプリケーションを開発・運用していた場合です。
現状公開されているアプリケーションは善意の開発者によって運営されていると信じています
まずユーザは画面に表示されているトランザクションに署名をしているつもりですが本当にそのトランザクションへと署名をしているのでしょうか?
画面に表示されているトランザクションへの署名に使われる保証はありません。
秘密鍵を直接入力する場合はもちろん暗号化秘密鍵とパスフレーズによって復号した秘密鍵を扱う場合でも開発者が悪意を持っている場合は秘密鍵をサーバーに送信する等の方法でユーザーから盗むことが可能です。
悪意のあるブラウザ拡張機能
Webアプリケーション開発者に悪意が無くともWebアプリケーション上で秘密鍵を入力すると盗まれる可能性があります。
ブラウザ拡張機能です。
ブラウザ拡張機能でWebアプリケーションへの入力を監視し取得することは不可能ではありません。
とある(Symbolブロックチェーンを導入している)Webアプリケーションを便利に使うためのブラウザ拡張機能の顔をして実際はそのアプリケーションで秘密鍵が入力されているであろう入力欄のIDから入力値を取得するプログラムをWebアプリケーション上で動作させることが技術的には可能なのです。
まぁこれはIDやパスワードとかにも言えることですね。
こんなことを言っているとなにも利用できないですね。
今日からできるリスクヘッジ
じゃあ使わないのかと言ったらそうもいきません。リスクと被害は最小限にして使います。
悪意を持った開発者は居ないと信じたいですが、現実は非情です。備えましょう。
- ブラウザを分ける
- シークレットウィンドウ
- 拡張機能をオフにする
- アカウントを分ける
1~3はブラウザ拡張機能が動作していない環境で秘密鍵を扱うことでブラウザ拡張機能経由で秘密鍵を盗まれる可能性への対処に侵入口を減らす対策です。
4は仮に盗まれても大きな損失にならないように、資産を管理するアカウントと資産の送受をするアカウントを分けて損失を低減する対処です。
マスクと早めのパブ□ンって感じですね。一次予防と二次予防をしましょう。
ここまで前書き
ここから本題
Webアプリケーション上で生成したトランザクションをブラウザ拡張機能上で署名する
ここからは私が研究で開発しているブラウザ拡張機能「SymbolSigner」に関する話をします。
現状、Symbolブロックチェーンを扱うWebアプリケーションを作成、利用するためには秘密鍵の取り扱いは無視できない問題です。
ユーザー視点では、利用するにあたってWebアプリケーション開発者の善性を全面的に信頼してアプリケーションを使うしかありません。
開発者視点では、秘密鍵を扱う以上悪意のないことを証明することは難しい(悪魔の証明的な)
本ブラウザ拡張機能の利用フロー
箇条書きと画面キャプチャで利用フローについて説明します。(記事の最後に動画がある)
利用者
- SymbolSignerのオプションページでアドレス、秘密鍵、パスワードを入力し暗号化秘密鍵とアドレスをSymbolSignerに登録する
- Webアプリケーションを利用する
- 署名が要求されるとSymbolSignerのポップアップ画面でパスワードを入力しログインする
- セットされたトランザクションの内容を確認し署名する
下図のページ(オプションページ)で暗号化秘密鍵とアドレスを登録します。よりセキュリティを高めるために設定はオフラインで行うことを推奨します。
ポップアップを開いたときの画面、設定したパスワードでログインします。
トランザクションがセットされている状態でUPDATEボタンを押下するとセットされたトランザクションの情報を確認できます。
SIGNボタンを押すとログイン時に入力されたパスワードと登録された暗号化秘密鍵を用いて復号した秘密鍵で拡張機能上で署名します。
開発者
- トランザクションを生成するWebアプリケーションを作成する
- 生成したトランザクションを引数にSymbolSigner.setTransaction()を実行する
- SymbolSigner.requestSign()を実行し拡張機能へトランザクションの署名を要求する
Webアプリケーション上でトランザクションを生成しています。
宛先と送信量、メッセージを入力しトランザクションを生成する簡単なWebアプリケーションのデモです
SET TXボタンを押下するとSymbolSigner.setTransaction(tx)
が実行されます。SET TXボタン押下後にREQ SIGNボタンを押下した時のキャプチャになります。画面右下に署名を要求されたことが通知されます。
ここでSymbolSigner.requestSign()
を実行しています。
要求した署名が完遂されたのちの画面です。
アプリケーションに署名済みのトランザクションが拡張機能から返却されています。
返却された署名済みトランザクションをノードにアナウンスすると一通りの動作が完了です。
生成したトランザクションのexplorerでの結果です。
Webアプリケーションを利用する際に秘密鍵を入力する・秘密鍵を構成する要素をWebアプリケーションへと渡すといったことをせずにトランザクションに署名しているためWebアプリケーションやWebアプリケーション上で動作するブラウザ拡張機能は秘密鍵を知る由もないです。
現状よりも安全にトランザクションへ署名ができていると言えます。
また、秘密鍵の入力は登録時のみとなるため署名が簡易になったと言えます。
技術っぽい話
ブラウザ拡張機能からWebページのコンテンツのwindowに対してSymbolSignerオブジェクト
を挿入し、SymbolSignerオブジェクトとブラウザ拡張機能でやり取りを行っています。
SymbolSignerには2つの関数があります。
- setTransaction
- requestSign
SymbolSigner.setTransaction(tx)
は引数に与えたトランザクションの情報をそのままjson化しブラウザ拡張機能へと送信しています。このjson化されたトランザクション情報からブラウザ拡張機能のバックグラウンドでトランザクションの再構築します。
SymbolSigner.requestSign()
は登録されたトランザクションへの署名を拡張機能に要請します。この関数は署名済みトランザクションをresolveするPromiseを発行します。署名が要求され一定時間以内にポップアップ画面からSIGNボタンを押下し署名されると署名済みトランザクションがWebアプリケーションへresolveされます。一定時間以内に署名が行われなかった場合セットされたトランザクションは破棄され、署名が行われなかったというメッセージがrejectされます。
ログイン
ログイン画面で入力した値の正当性の検証が必要になります。
当然ですが、パスワードは拡張機能に保存していないので単純に入力した値と比較することはできません。
秘密鍵、パスワードを入力し暗号化秘密鍵とアドレスをSymbolSignerに登録する
と利用者の説明で書きました。そうですアドレスを保存しています。
アドレスは秘密鍵から導出できます。
なので、保存している暗号化秘密鍵を入力値で復号した文字列から導出したアドレスと保存しているアドレスが一致すればパスワードが合っているかがわかります。
パスワードが一致すれば、拡張機能のlocalStorageに入力したパスワードとタイムスタンプを保存します。
タイムスタンプで一定時間後に自動でログアウトするようにしています。
設定ページ
アドレス、秘密鍵、パスワードを入力し、アドレスと暗号化秘密鍵を保存します。
秘密鍵からアドレスが導出できるため本来はアドレスの入力欄は不要なのですが、
秘密鍵の誤入力やコピペのミスが合った際に目視確認ではミスを見つけにくいため、秘密鍵から導出されるアドレスと
入力したアドレスが一致しなかった場合は保存できないようにして設定のミスを防いでいます。
これらの値は拡張機能のストレージ領域へと保存するためオフライン環境でも動作するので念押しでオフライン状態での作業を推奨しています。
アプリケーションにSymbolSignerを導入する
トランザクション生成~登録のプログラム 雑な実装ですが許して
function btnFunc() {
const NETWORK_TYPE = symbol.NetworkType.TEST_NET
const to = document.getElementById('toAddr')
const msg = document.getElementById('msg')
const xym = new symbol.Mosaic(new symbol.MosaicId('3A8416DB2D53B6C8'), symbol.UInt64.fromUint(1000000))
getInfo().then(data => {
const tx = symbol.TransferTransaction.create(
symbol.Deadline.create(data.epochAdjustment),
symbol.Address.createFromRawAddress(to.value),
[xym],
symbol.PlainMessage.create(msg.value),
NETWORK_TYPE,
symbol.UInt64.fromUint(2000000)
)
SymbolSigner.setTransaction(tx)
})
}
署名要求部分のプログラム
const requestSign = () => {
console.log('reqsign')
SymbolSigner.requestSign().then((res) => {
console.log('成功:', res)
const a = document.createElement('a')
const link = `https://testnet.symbol.fyi/transactions/${res.txHash}`
a.innerText = `txHash: ${res.txHash}`
a.href = link
a.target = '_blank'
const div = document.getElementById('txHash')
const p = document.createElement('p')
p.innerText = JSON.stringify(res.signedTx)
div.appendChild(a)
div.appendChild(p)
console.log('announce page')
new symbol.TransactionHttp(NODEURL)
.announce(res.signedTx)
.subscribe((x) => console.log('x', x), (err) => console.error(err));
}).catch((err) => {
console.error(err)
}).finally(() => {
console.log('finally')
})
}
getInfo()
const getInfo = () => {
return new Promise((resolve, reject) => {
const NODEURL = 'https://sym-test.opening-line.jp:3001'
const repositoryFactory = new symbol.RepositoryFactoryHttp(NODEURL);
repositoryFactory.getGenerationHash().toPromise().then(gh => {
repositoryFactory.getCurrencies().toPromise().then(cur => {
repositoryFactory.getEpochAdjustment().toPromise().then(ep => {
resolve({
generationHash: gh,
currency: cur,
epochAdjustment: ep
})
})
}).catch(() => {
reject()
})
}).catch(() => {
reject()
})
}).catch(() => {
reject()
})
}
React製のWebアプリケーションに導入する
React + muiで作成したWebアプリケーションでSymbolSignerを使用する選択肢の追加
Use SymbolSignerのトグルスイッチをオンにするとSymbolSignerでの署名になります。
プログラム
import { Button, Container, TextField, Typography, FormControlLabel, Switch } from '@mui/material';
import { styled } from '@mui/system';
import { useState } from 'react'
import {
Account,
Deadline,
Address,
NetworkType,
TransferTransaction,
MosaicId,
Mosaic,
PlainMessage,
UInt64,
TransactionHttp
} from 'symbol-sdk';
function App() {
const [priKey, setPriKey] = useState('')
const [addr, setAddr] = useState('')
const [msg, setMsg] = useState('')
const [flag, setFlag] = useState(false)
const NODEURL = "https://sym-test.opening-line.jp:3001"
const sendTx = () => {
const tx = TransferTransaction.create(
Deadline.create(1637848847), // epoch adjustment
Address.createFromRawAddress(addr),
[
new Mosaic(
new MosaicId("3A8416DB2D53B6C8"), // xym
UInt64.fromUint(10 * Math.pow(10, 6))
),
],
PlainMessage.create(msg),
NetworkType.TEST_NET,
UInt64.fromUint(2000000)
);
if(flag) {
console.log('use SymbolSigner')
// eslint-disable-next-line no-undef
SymbolSigner.setTransaction(tx)
// eslint-disable-next-line no-undef
SymbolSigner.requestSign().then(signedTx => {
new TransactionHttp(NODEURL).announce(signedTx).subscribe(
(x) => console.log("x", x),
(err) => console.error(err)
)
})
} else {
const master = Account.createFromPrivateKey(priKey, NetworkType.TEST_NET);
console.log(master.privateKey)
const signedTx = master.sign(
tx,
"7FCCD304802016BEBBCD342A332F91FF1F3BB5E902988B352697BE245F48E836" // generation hash
);
new TransactionHttp(NODEURL).announce(signedTx).subscribe(
(x) => console.log("x", x),
(err) => console.error(err)
)
}
}
return (
<Container>
<Typography variant="h2" component="div" gutterBottom>
SymbolSigner Demo in React
</Typography>
<FormControlLabel
control={<Switch defaultChecked checked={flag}/>}
label="Use SybolSigner"
onChange={(e) => setFlag(e.target.checked)}
/>
<Typography variant="h4" component="div" gutterBottom>
{flag ? "Use SymbolSigner" : "Use PrivateKey"}
</Typography>
{flag || (
<Wrapper>
<TextField
label="PrivateKey"
fullWidth
onChange={(e) => setPriKey(e.target.value)}
/>
</Wrapper>
)}
<Wrapper>
<TextField
label="Address"
fullWidth
onChange={(e) => setAddr(e.target.value)}
/>
</Wrapper>
<Wrapper>
<TextField
label="Message"
fullWidth
onChange={(e) => setMsg(e.target.value)}
/>
</Wrapper>
<Wrapper>
<Button variant="outlined" onClick={sendTx}>SIGN TX</Button>
</Wrapper>
</Container>
)
}
export default App
const Wrapper = styled("div")({
margin: "16px"
})
研究っぽい話
少しは研究っぽいことも書いときます。
簡便性と安全性
SymbolSignerを導入する前後での簡便性と安全性を比較します。
簡便性の観点
現在は署名の際に秘密鍵を入力する、もしくはパスフレーズで暗号化した暗号化秘密鍵をlocalStorageに保存しパスフレーズを用いて復号しています。
秘密鍵を入力している場合、署名を行う度に64字の秘密鍵を入力する必要性があるが、SymbolSignerでは何度署名が必要となろうと、秘密鍵を入力するのは設定を行う場合のみである。
暗号化秘密鍵を使用している場合はパスフレーズを入力する点は変化しないためほとんど簡便性の変化はないと言える。
安全性の観点
導入前後でのリスク比較
Webアプリケーション上で秘密鍵を入力するリスク | 攻撃者 | 既存システム | SymbolSigner |
---|---|---|---|
入力した秘密鍵のサーバーへの送信 | WebApp, Extension | × | 〇 |
入力した秘密鍵を使い画面上に表示されていないトランザクションへ署名する | Web, Extension | × | 〇 |
マルウェアによる入力監視 | マルウェア | × | × |
設定値を盗む | ブラウザ拡張機能(開発者画面) | - | △ |
上3つはまぁ、言わずもがななので4つ目の設定値を盗むについて少し
ブラウザ上で動作するブラウザ拡張機能はブラウザ拡張機能のオプションページや拡張機能ポップアップ上では動作していません。
しかし開発者ウィンドウを拡張する拡張機能はオプションページでも開発者ウィンドウを開くとプログラムが動作します。
なので悪意のある拡張機能が入った状態でユーザーがオプションページ内で特定の動作(開発者ウィンドウや悪意のある拡張機能のポップアップ)を行うと暗号化秘密鍵を盗むことは可能なので変なことをすると危険性は少々あります。
導入するメリット
利用者は言わずもがな安全に署名ができるので使うメリットがあると言えるのですが、開発者としてもメリットがあります。
SymbolSignerで署名をすることによって秘密鍵を盗むことができなくなるため、僕は悪い開発者じゃないよってことがアピールできるかと思います。
また、秘密鍵の扱いについて考える必要がなくなることもメリットっちゃメリットかもですね。
今後の展望
卒論かいて(ry
SymbolSignerでやりたいことやるべきこと書いていきます。
対応トランザクションを増やす
研究として作成している範囲的に現状転送トランザクションへしか対応できていないので、他のトランザクションへも対応したい。
トランザクション情報のjsonをパースして再構築する部分は分離しているためやる気を出せばかける。卒論発表終わったらがんばる。
ハードウェアウォレットとの連携
SymbolSignerの署名方法として暗号化秘密鍵の使用とハードウェアウォレットで署名を選べるようにしたい。
既存実装としてSymbolWalletがあるため可能ではあると見てる。ただ全然わからんので時間が掛かりそう。
SymbolSignerで開発者が簡易なAPIで署名処理を移譲できるようにはなったがSymbolSignerを使用しているから盗まれる可能性が0になるというわけでは無いのでユーザーへの選択肢としてハードウェアウォレットでの署名を提供できるようにしたい。
Reactにする
UI改善等、現在html,css,js + tailwindcssでUIを作っているが、書きにくいし読みにくいのでReact + muiとかで画面は作り直したい。
アドレスの複数登録
アドレスを複数登録して署名する時に選択できる方が便利。ログイン時にアドレス選んでログイン、ログアウト機能を作るか、ログイン画面をなくしてアドレスを選択してパスフレーズを入力して署名することになる。
リリース
いくら高尚なアイデアでも頭の中にあるだけなら無いも同然って話で、作ったものもリリースしなければ存在しないと同義なので。。。
大学院始まるまでにリリースしたい。
終わりに
画像だとわかりにくいのでデモ動画上げときました。
Webアプリケーション上で署名を行うことって実は危険で現状は開発者を信用してアプリケーションを利用しています。Webアプリ開発者が良い開発者であろうとブラウザ拡張機能をむやみに入れると結構怖いです。
なので、現状より安全で簡易に署名を行うための研究でブラウザ拡張機能を作成しました。
現状の秘密鍵を入力するかは、基本的には経歴や周りの評価等を根拠として信頼することになるのですかね?
まぁ、何を根拠に信頼するのかとかいうと果たしてChromeは安全なのか?Windowsは安全なのか?とキリがなかったりします。
自分が納得できるように検索したり、実装を読んだりして信頼できる根拠を見つける必要があるかもしれないです。
Webアプリに機密情報を渡さ無いために特定の場所に機密情報を渡すってパスワードマネージャーみたいですね。
では、僕たちは何を根拠にパスワードマネージャーが安全であると信頼しているのでしょうか?