First public release

This commit is contained in:
Inndy
2020-05-20 00:11:26 +08:00
commit 7fc247b152
28 changed files with 1762 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
__pycache__
/venv
/swigwin-4.0.1
/swigwin-4.0.1.zip

15
LICENSE Normal file
View File

@ -0,0 +1,15 @@
This file is part of twnhi-smartcard-agent.
twnhi-smartcard-agent is free software: you can redistribute it and/or
modify it under the terms of the GNU General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
twnhi-smartcard-agent is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with twnhi-smartcard-agent.
If not, see <https://www.gnu.org/licenses/>.

229
README.md Normal file
View File

@ -0,0 +1,229 @@
# TWNHI Smartcard Agent
## 這是什麼? / What is this?
這是一個可以取代
[健保卡讀卡機元件](https://cloudicweb.nhi.gov.tw/cloudic/system/SMC/mEventesting.htm)
的程式,使用 Python 重新撰寫,避開了原始實作的軟體缺陷,提供更好的品質與文件。
## TODO
- [x] 增加 socks5 proxy 伺服器,攔截 `iccert.nhi.gov.tw` 的連線 /
Add socks5 proxy to hijack connection to `iccert.nhi.gov.tw`
- [ ] 完善文件 / Finish documents
- [x] 驗證 client 來自 `*.gov.tw` / Limit the connection was came from `*.gov.tw`
- [ ] 預編譯 `pyscard` 套件 / Prebuild `pyscard` package
- [ ] 製作 prebuilt package 並加入 GitHub releases /
Prebuild package and add to GitHub releases
- [ ] 蒐集使用回饋 / Collect usage feedbacks
## 相依套件 / Dependencies
- `python>=3.6` (Only tested on Python 3.8.0)
- `openssl>=1.1`
- `virtualenv`
- `requirements.txt` 檔案內列出的 Python 套件
- Windows 使用者需要 Visual Studio 或者
[Microsoft C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/)
## 我很懶,給我懶人包 / TL;DR
```
# Windows (PowerShell)
> Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process -Force
> python.exe install-packages.py
> .\venv\Scripts\activate.ps1
> python.exe server.py
# Linux (Ubuntu)
$ sudo apt-get install libpcsclite-dev
# Linux and macOS
$ brew install swig # homebrew/linuxbrew
$ python3 install-packages.py
$ source ./venv/bin/activate
$ python3 server.py
```
## 安裝方式 / Setup
### 範例指令格式 / Note for the commands listed below
```
> python.exe --version # this command is for Windows
$ python3 --version # this command is for Linux and macOS
```
### 確認 Python 版本大於等於 3.6 / Check python version
```
> python.exe --version
$ python3 --version
Python 3.8.0
```
### 確認有沒有安裝好 virtualenv / Check virtualenv
```
> python.exe -m virtualenv
$ python3 -m virtualenv
python3: No module named virtualenv
```
### 安裝 virtualenv / Install virtualenv
```
> python.exe -m pip install virutalenv
$ python3 -m pip install virutalenv
(很長的安裝畫面... / Installation progress...)
```
### 確認有沒有安裝好 virtualenv / Check virtualenv again
```
> python.exe -m virutalenv
$ python3 -m virtualenv
usage: virtualenv [--version] [--with-traceback] [-v | -q] [--app-data APP_DATA] [--clear-app-data]
[--discovery {builtin}] [-p py] [--creator {builtin,cpython3-win,venv}]
[--seeder {app-data,pip}] [--no-seed] [--activators comma_sep_list] [--clear]
[--system-site-packages] [--symlinks | --copies] [--no-download | --download]
[--extra-search-dir d [d ...]] [--pip version] [--setuptools version]
[--wheel version] [--no-pip] [--no-setuptools] [--no-wheel] [--symlink-app-data]
[--prompt prompt] [-h]
dest
virtualenv: error: the following arguments are required: dest
```
### 建立一個 virtualenv / Create a virtualenv
```
> python.exe -m virtualenv venv
$ python3 -m virtualenv venv
created virtual environment CPython3.6.8.final.0-64 in 3169ms
creator CPython3Posix(dest=/code/venv, clear=False, global=False)
seeder FromAppData(download=False, pip=latest, setuptools=latest, wheel=latest, via=copy, app_data_dir=/home/user/.local/share/virtualenv/seed-app-data/v1.0.1)
activators BashActivator,CShellActivator,FishActivator,PowerShellActivator,PythonActivator,XonshActivator
```
### 啟動 virtualenv / Activate virtualenv we just created
```
> .\venv\Scripts\activate.ps1 # Windows with Powershell
$ source ./venv/bin/activate # Linux and macOS, I assume you are using a POSIX shell
```
#### PowerShell 錯誤 / PowerShell Error
PowerShell 預設不允許執行不信任的 script如果你發生以下錯誤請執行
`Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process -Force`
,這將會允許現在的 PowerShell process 執行外部 script
Default config of PowerShell does not allow external script execution.
Execute `Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process -Force`
to temporarily allow execution of external script.
```
PS C:\code> .\venv\Scripts\activate.ps1
.\venv\Scripts\activate.ps1 : 因為這個系統上已停用指令碼執行,所以無法載入 C:\code\venv\Scripts\activate.ps1 檔案。如需詳細資訊,請參閱 about_Execution_Policies網址為 https:/go.microsoft.com/fwl
ink/?LinkID=135170。
位於 線路:1 字元:1
+ .\venv\Scripts\activate.ps1
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : SecurityError: (:) [], PSSecurityException
+ FullyQualifiedErrorId : UnauthorizedAccess
PS C:\code> Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process -Force
PS C:\code> .\venv\Scripts\activate.ps1
(venv) PS C:\code>
```
### 安裝 Swig / Install Swig
[pyscard](https://github.com/LudovicRousseau/pyscard/blob/master/INSTALL.md#installing-on-gnulinux-or-macos-from-the-source-distribution)
需要使用到 [Swig](http://www.swig.org/) 來產生 Python native extension
macOS 使用者推薦使用 [Homebrew](https://brew.sh/) 來安裝套件Linux 使用者也可以 Homebrew
([Linuxbrew 目前已經與 Homebrew 合併](https://github.com/Linuxbrew/brew/issues/612))
使用發行版自帶的套件管理工具 (apt, rpm, pacman... etc.) 來安裝 swig。
```
$ brew install swig # Use homebrew/linuxbrew to install swig
```
### 安裝需要的套件 / Install python packages
```
$ sudo apt-get install libpcsclite-dev # Linux need libpcsclite-dev, apt-get is for Ubuntu
$ pip install -r requirements.txt
```
## 使用方式 / Usage
1. 啟動 virtualenv / Activate the virtualenv
2. 執行 server.py / Run server.py
3. 設定瀏覽器使用 socks5 proxy 127.0.0.1:17777 /
Config your browser to use 127.0.0.1:17777 as socks5 proxy
### 設定瀏覽器使用 socks5 proxy / Config your browser to use socks5 proxy
#### Chrome
- [Proxy SwitchyOmega](https://chrome.google.com/webstore/detail/proxy-switchyomega/padekgcemlokbadohgkifijomclgjgif)
- [FoxyProxy Standard](https://chrome.google.com/webstore/detail/foxyproxy-standard/gcknhkkoolaabfmlnjonogaaifnjlfnp)
See [How to setup socks5 proxy in Chrome with FoxyProxy](docs/setup-socks5-proxy-chrome-foxyproxy.md)
#### Firefox
> TBD
### 設定系統信任 root certificate / Config your system to trust our root certificate
#### Windows
```
> cd certs
> .\trust_ca.cmd
```
#### macOS
```
$ cd certs
$ ./trust_ca_macos.sh
```
### Linux
```
# For Ubuntu and Firefox
$ sudo apt-get install libnss3-tools
$ cd certs
$ ./trust_ca_ubuntu_firefox.sh
```
關閉瀏覽器,再重新開啟設定才會生效
### 啟動伺服器並進行測試
```
> python server.py
```
設定好 socks5 porxy並且用瀏覽器開啟
[https://iccert.nhi.gov.tw:7777/](https://iccert.nhi.gov.tw:7777/)
正確設定的狀況下應該不會看到任何錯誤,並且看到 `It works!` 就表示 agent 啟動成功
## 資訊安全考量 / Security Issue
### 自簽憑證 / Self-signed Certificate
由於健保卡讀卡機元件使用 wss (WebSocket Secure) 通訊協定,因此必須要有 SSL/TLS 憑證,
目前健保署並未提供 `iccert.nhi.gov.tw` 的有效憑證,因此我們使用自簽憑證來處理這個問題。
為了使用方便,安裝步驟中會引導使用者在系統上安裝並信任自簽根憑證,為了使用者的方便,
已經有一組預先產生好的憑證可以使用,為了確保該憑證不會被濫用,我們已將根憑證的私鑰銷毀。
若您希望有更高的安全性,可以參考 certs 目錄底下的 Makefile裡面有使用 openssl
重新產生一組私鑰與憑證的方法,自行產生自己的根憑證與網站憑證,
再銷毀根憑證的私鑰來保證自簽根憑證不會遭到竊取與盜用。
以下是重新產生憑證的步驟:
```
> cd certs
> make clean
> make all
# 現在可以參考上面的步驟,讓系統信任剛剛產生的 CA
> ./trust_ca_macos.sh # 以 macOS 為例
```
## 授權 / License
[GPL v3](LICENSE)

7
certs/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
*.crt
*.key
ca.srl
!ca.crt
!chain.crt
!host.crt
!host.key

36
certs/Makefile Normal file
View File

@ -0,0 +1,36 @@
DAYS ?= 730
all: host.crt check chain.crt
clean:
rm ca.key ca.crt host.key host.crt host.csr chain.crt
finalize: host.crt
rm ca.key
rm chain.crt
$(MAKE) chain.crt
: Now you can trust ca.crt in your system, and nobody can abuse this root CA
ca.key:
openssl genrsa -out ca.key 4096
ca.crt: ca.key
openssl req -x509 -new -nodes -key ca.key -sha256 -days $(DAYS) -out ca.crt -subj "/C=TW/ST=Taiwan/O=Inndy's NHI Smartcard Client"
host.key:
openssl genrsa -out host.key 4096
host.csr: host.key
openssl req -new -key host.key -config san.cnf -sha256 -out host.csr
host.crt: host.csr ca.crt ca.key
openssl x509 -req -in host.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out host.crt -days $(DAYS) -sha256 -extensions req_ext -extfile san.cnf
chain.crt:
cat host.crt ca.crt > chain.crt
check:
: ==================== ca.crt ====================
openssl x509 -noout -text -in ca.crt
: ==================== host.crt ====================
openssl x509 -noout -text -in host.crt

29
certs/ca.crt Normal file
View File

@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIFBjCCAu4CCQCRhyBUzRq1gjANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJU
VzEPMA0GA1UECAwGVGFpd2FuMSUwIwYDVQQKDBxJbm5keSdzIE5ISSBTbWFydGNh
cmQgQ2xpZW50MB4XDTIwMDUwNjE0NTMzM1oXDTIyMDUwNjE0NTMzM1owRTELMAkG
A1UEBhMCVFcxDzANBgNVBAgMBlRhaXdhbjElMCMGA1UECgwcSW5uZHkncyBOSEkg
U21hcnRjYXJkIENsaWVudDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
AK5KShoUDkJ1U8kvzISbDTk9z1QMYGo/jNk3oqL3YS4nhFQt9VSIN8k0Z+ehCKlV
tDQK0sWVUPG8f/lVtKV+MSGMOuQTp0rtUYpTRrbmv/FblF5atI0c3jo0beP47fH7
1qTYFZep7tR4PFG9U9kDsQCR0nam35ydqq9j/ehBp6/QGAg1gMrhuxj29VefpE9+
GQXEM41MUIW53ZOlRB8MJkFCfxwfxUbMfuA/JdAkmiRXpM6IpffVfSdhISQPtv6O
ZPs2yHxYp7HOuV7XOxV/34o/A/wiEjoz56eoH+oslgjzB/enk9oBSwkAfYL1FFDN
hWNspkRLEhairRbQuve46l3i7g0jVxUcVIV6PcyC+C9Ci2L9dGX/HBlgmfo8FlX7
ai2NDujjWGxGp3iFa7wvU/sDZar8ViEUo4FKKgPOY1cRpalBi8uEFXfS9FRh9ckV
rxZDxWsybLwkA+JrfNtKi5XAbJyoHQSG8f7XazuSsh7aLFMNkaHZ1lks3y2nWB+M
z5a89P8Lpj9AXFr/M3LWn9RFg7u+2LCLGFiUmaHG/JfDKxlDTyFh2K+UrgG2c4Ek
9EuehsUVjmJCoMZ1inVDkPixFNuRvlh+TTI0cU+ULVUFtdU5heOuaXXNHV5UDHID
8gucoNJb1aqpNoMtSDNYd5aaxuGwgwMHL/RjwC7lWOj1AgMBAAEwDQYJKoZIhvcN
AQELBQADggIBABibU0zYYGogbwDf+xj/38D9KdpedIW2ZL86ExQyySYgdZm2eOTa
e+qVVIzVOoKQFPtIAXX07M7EDJVIte79eyZnAHWNA0QEgTGBwrltybFyN+PQNpjS
+b+2YsiqB4mtxmkOf4uypT+cUwYBw2Y/yD69lPHtsbYcboud20qzsw28/72sShUY
yfJ9E0BtjsagoTNniUnXx6O3uIN9nzgdC98n9qTRXQqo1YoidsoK+nnY+g0jAYNh
QzyPxinppbM9TGpEWhs1FGaQasTEX/eCbePwPvb5zRKDBplTg1IAATx9njzhhmet
hrYj9elEsWCwV0d7NMp7zbr8pBHAdbvs3DpcPMKpSNA4918Ak89dfRXuKxyvsbRE
e3tr0pLL2NC+9V2C2yj+U/lf+8xUzwBxSbgEjStmwdVPtJj2897iNSCDoKlC3vlY
uaJL0ShEp4FNpdUwa1RB9dSSFeYC/s/FNgk/pcEblE820rP/Y+IMtcmYcTg5c7ta
0g3Scl+JBp0y3fuGhSSZ7iyhqM6RZEKHYTnLbE1mOKx1wAhb0pQ9UC0+EbT/elJs
5EtcLtmgLidA2YKc7u4uJhNvmIIYKE1XQNytsP4PxGjlYJh5C+QzqxwcjfIza+Xc
r/a5yG+yjvxet0lvr04JgHQjvYW6Sqvwn8J+Ioeh8+ZCx/cteEQg9gpl
-----END CERTIFICATE-----

60
certs/chain.crt Normal file
View File

@ -0,0 +1,60 @@
-----BEGIN CERTIFICATE-----
MIIFRDCCAyygAwIBAgIJAN6l3sQW0+nHMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAlRXMQ8wDQYDVQQIDAZUYWl3YW4xJTAjBgNVBAoMHElubmR5J3MgTkhJIFNt
YXJ0Y2FyZCBDbGllbnQwHhcNMjAwNTA2MTQ1MzMzWhcNMjIwNTA2MTQ1MzMzWjBE
MQswCQYDVQQGEwJUVzEPMA0GA1UECAwGVGFpd2FuMSQwIgYDVQQKDBtJbm5keXMg
TkhJIFNtYXJ0Y2FyZCBDbGllbnQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
AoICAQCufr1KhcY3TcASUwv9RyidgKf9FESn+r97V+QGFpMhewxtAtVnpZIfNr5U
Mvz1sIQ1MmKur2bvcxLrRjfrncJHKmJpBJscZGvViIV4ziIi9bdvpQHVdp4qrLwV
lJOFG5bCirKRuTeC0HRoZ6/1S71NzBwpKsgcoXLmd6PoqUFih6qphoWgN6yo5PQo
/HbHszaVrcTGwdsQBaWzoTihRDeC9lzuRSN4s+3jJvh7H0GRlFsi9suK2KJaQXw9
BrRGD34gPJMYjaTRTVUyDAI0bW1vsN0GAtKxTfoB3VRCIEbP4DUtOPYI9e0rw2JA
t8oUd4gvaRWTSakOMuBPLssrJaFaQ0CHVrsvuoXJ8gSHPu4pFX0MvPLwXDpvyFWD
Z9B8u7PwIc39Jtq1WAgS/i6+xNQzo7djp+Jzgh8VuMIg1iyzV+jJIhy1WdyoKXl6
p3cg5DjzF09F3s9Of/3rHpVPnoSwrvKOG/ND2l/TQyEOV9JuStqO27wOZudAd42G
oJX8dn9P/r+MPtOPBFFK1J8jm28aK7KptXN84gV/IH6nEHf9vAcDHy/RaQE9fIYu
pudTWwAhgSdH5i4QcPlpXpQdzSD7PC9uUEdiCHaeze4UWc9N0Qw+oH93bN2MqxsU
Y/9SqfTWVCKQAs4WKF7+C3sB1+gfs0P2/RDuOtHJfSQVQSrlfQIDAQABozgwNjAJ
BgNVHRMEAjAAMAsGA1UdDwQEAwIFoDAcBgNVHREEFTATghFpY2NlcnQubmhpLmdv
di50dzANBgkqhkiG9w0BAQsFAAOCAgEALf5LBL74y9uupYrxYWkzvnWwAnwEVQ1V
g2ygBP7ldrCQIPof69UuWZ2CwN70NwH3AtI6vd2cUBpH2AYyaJe3qYsfsq+/xpdv
FB5xVoIjA3MBuzZWW+PZhtTQ41pzsuKH5MG70SYk/y27/VqQmFTzNBAXLc1130sn
o6uJflYcVE+XxfEj901Zz1BdIcpfu44bqz8ilAaDU6bG4IOBnKQ18GNbfO7Fj+Ei
28UcoGxaiiAcCIektZitaNKjVb4U6uIpNuxhAHxPCZq24z19Iwa4+1eI1GzoxhCW
SFmAZFuF26KVqAOH5Ewo9AlAQ59L99vuhHpyhdt4bXyJCGKYVTJMkEf2oLTaOLoX
1aEpIyCDFY611jJBEEGssajcpZBQWuaK1zTCHUZWp+l1Tme90C5XRPfnCbuei31o
SRK+mDXh1JRWyJxIelta46X3tTN+rEzSnMjUeR1dto8Z54iXu3VctcOfZFrUsKMB
uiwLtN6vYT0jIqlI13C24ybePn6v+1HIc29KNblQVwBpBcU2zcQ6Y2/DAL4NSDBS
hmET24hK/6RpBW+foWxRK6Jl2MSbj+thRBIGKQcAMGs0WOaVk9DNqvOgYGJFixec
HiKJusY06F6ecvC0aaKmbX2kRJf1CvIQ+Tgoz3IVTzyaGZIrqDdg7VZncwXNPd6b
ze8ixlU56U0=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFBjCCAu4CCQCRhyBUzRq1gjANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJU
VzEPMA0GA1UECAwGVGFpd2FuMSUwIwYDVQQKDBxJbm5keSdzIE5ISSBTbWFydGNh
cmQgQ2xpZW50MB4XDTIwMDUwNjE0NTMzM1oXDTIyMDUwNjE0NTMzM1owRTELMAkG
A1UEBhMCVFcxDzANBgNVBAgMBlRhaXdhbjElMCMGA1UECgwcSW5uZHkncyBOSEkg
U21hcnRjYXJkIENsaWVudDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
AK5KShoUDkJ1U8kvzISbDTk9z1QMYGo/jNk3oqL3YS4nhFQt9VSIN8k0Z+ehCKlV
tDQK0sWVUPG8f/lVtKV+MSGMOuQTp0rtUYpTRrbmv/FblF5atI0c3jo0beP47fH7
1qTYFZep7tR4PFG9U9kDsQCR0nam35ydqq9j/ehBp6/QGAg1gMrhuxj29VefpE9+
GQXEM41MUIW53ZOlRB8MJkFCfxwfxUbMfuA/JdAkmiRXpM6IpffVfSdhISQPtv6O
ZPs2yHxYp7HOuV7XOxV/34o/A/wiEjoz56eoH+oslgjzB/enk9oBSwkAfYL1FFDN
hWNspkRLEhairRbQuve46l3i7g0jVxUcVIV6PcyC+C9Ci2L9dGX/HBlgmfo8FlX7
ai2NDujjWGxGp3iFa7wvU/sDZar8ViEUo4FKKgPOY1cRpalBi8uEFXfS9FRh9ckV
rxZDxWsybLwkA+JrfNtKi5XAbJyoHQSG8f7XazuSsh7aLFMNkaHZ1lks3y2nWB+M
z5a89P8Lpj9AXFr/M3LWn9RFg7u+2LCLGFiUmaHG/JfDKxlDTyFh2K+UrgG2c4Ek
9EuehsUVjmJCoMZ1inVDkPixFNuRvlh+TTI0cU+ULVUFtdU5heOuaXXNHV5UDHID
8gucoNJb1aqpNoMtSDNYd5aaxuGwgwMHL/RjwC7lWOj1AgMBAAEwDQYJKoZIhvcN
AQELBQADggIBABibU0zYYGogbwDf+xj/38D9KdpedIW2ZL86ExQyySYgdZm2eOTa
e+qVVIzVOoKQFPtIAXX07M7EDJVIte79eyZnAHWNA0QEgTGBwrltybFyN+PQNpjS
+b+2YsiqB4mtxmkOf4uypT+cUwYBw2Y/yD69lPHtsbYcboud20qzsw28/72sShUY
yfJ9E0BtjsagoTNniUnXx6O3uIN9nzgdC98n9qTRXQqo1YoidsoK+nnY+g0jAYNh
QzyPxinppbM9TGpEWhs1FGaQasTEX/eCbePwPvb5zRKDBplTg1IAATx9njzhhmet
hrYj9elEsWCwV0d7NMp7zbr8pBHAdbvs3DpcPMKpSNA4918Ak89dfRXuKxyvsbRE
e3tr0pLL2NC+9V2C2yj+U/lf+8xUzwBxSbgEjStmwdVPtJj2897iNSCDoKlC3vlY
uaJL0ShEp4FNpdUwa1RB9dSSFeYC/s/FNgk/pcEblE820rP/Y+IMtcmYcTg5c7ta
0g3Scl+JBp0y3fuGhSSZ7iyhqM6RZEKHYTnLbE1mOKx1wAhb0pQ9UC0+EbT/elJs
5EtcLtmgLidA2YKc7u4uJhNvmIIYKE1XQNytsP4PxGjlYJh5C+QzqxwcjfIza+Xc
r/a5yG+yjvxet0lvr04JgHQjvYW6Sqvwn8J+Ioeh8+ZCx/cteEQg9gpl
-----END CERTIFICATE-----

31
certs/host.crt Normal file
View File

@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFRDCCAyygAwIBAgIJAN6l3sQW0+nHMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAlRXMQ8wDQYDVQQIDAZUYWl3YW4xJTAjBgNVBAoMHElubmR5J3MgTkhJIFNt
YXJ0Y2FyZCBDbGllbnQwHhcNMjAwNTA2MTQ1MzMzWhcNMjIwNTA2MTQ1MzMzWjBE
MQswCQYDVQQGEwJUVzEPMA0GA1UECAwGVGFpd2FuMSQwIgYDVQQKDBtJbm5keXMg
TkhJIFNtYXJ0Y2FyZCBDbGllbnQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
AoICAQCufr1KhcY3TcASUwv9RyidgKf9FESn+r97V+QGFpMhewxtAtVnpZIfNr5U
Mvz1sIQ1MmKur2bvcxLrRjfrncJHKmJpBJscZGvViIV4ziIi9bdvpQHVdp4qrLwV
lJOFG5bCirKRuTeC0HRoZ6/1S71NzBwpKsgcoXLmd6PoqUFih6qphoWgN6yo5PQo
/HbHszaVrcTGwdsQBaWzoTihRDeC9lzuRSN4s+3jJvh7H0GRlFsi9suK2KJaQXw9
BrRGD34gPJMYjaTRTVUyDAI0bW1vsN0GAtKxTfoB3VRCIEbP4DUtOPYI9e0rw2JA
t8oUd4gvaRWTSakOMuBPLssrJaFaQ0CHVrsvuoXJ8gSHPu4pFX0MvPLwXDpvyFWD
Z9B8u7PwIc39Jtq1WAgS/i6+xNQzo7djp+Jzgh8VuMIg1iyzV+jJIhy1WdyoKXl6
p3cg5DjzF09F3s9Of/3rHpVPnoSwrvKOG/ND2l/TQyEOV9JuStqO27wOZudAd42G
oJX8dn9P/r+MPtOPBFFK1J8jm28aK7KptXN84gV/IH6nEHf9vAcDHy/RaQE9fIYu
pudTWwAhgSdH5i4QcPlpXpQdzSD7PC9uUEdiCHaeze4UWc9N0Qw+oH93bN2MqxsU
Y/9SqfTWVCKQAs4WKF7+C3sB1+gfs0P2/RDuOtHJfSQVQSrlfQIDAQABozgwNjAJ
BgNVHRMEAjAAMAsGA1UdDwQEAwIFoDAcBgNVHREEFTATghFpY2NlcnQubmhpLmdv
di50dzANBgkqhkiG9w0BAQsFAAOCAgEALf5LBL74y9uupYrxYWkzvnWwAnwEVQ1V
g2ygBP7ldrCQIPof69UuWZ2CwN70NwH3AtI6vd2cUBpH2AYyaJe3qYsfsq+/xpdv
FB5xVoIjA3MBuzZWW+PZhtTQ41pzsuKH5MG70SYk/y27/VqQmFTzNBAXLc1130sn
o6uJflYcVE+XxfEj901Zz1BdIcpfu44bqz8ilAaDU6bG4IOBnKQ18GNbfO7Fj+Ei
28UcoGxaiiAcCIektZitaNKjVb4U6uIpNuxhAHxPCZq24z19Iwa4+1eI1GzoxhCW
SFmAZFuF26KVqAOH5Ewo9AlAQ59L99vuhHpyhdt4bXyJCGKYVTJMkEf2oLTaOLoX
1aEpIyCDFY611jJBEEGssajcpZBQWuaK1zTCHUZWp+l1Tme90C5XRPfnCbuei31o
SRK+mDXh1JRWyJxIelta46X3tTN+rEzSnMjUeR1dto8Z54iXu3VctcOfZFrUsKMB
uiwLtN6vYT0jIqlI13C24ybePn6v+1HIc29KNblQVwBpBcU2zcQ6Y2/DAL4NSDBS
hmET24hK/6RpBW+foWxRK6Jl2MSbj+thRBIGKQcAMGs0WOaVk9DNqvOgYGJFixec
HiKJusY06F6ecvC0aaKmbX2kRJf1CvIQ+Tgoz3IVTzyaGZIrqDdg7VZncwXNPd6b
ze8ixlU56U0=
-----END CERTIFICATE-----

28
certs/host.csr Normal file
View File

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIE0DCCArgCAQAwRDELMAkGA1UEBhMCVFcxDzANBgNVBAgMBlRhaXdhbjEkMCIG
A1UECgwbSW5uZHlzIE5ISSBTbWFydGNhcmQgQ2xpZW50MIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEArn69SoXGN03AElML/UconYCn/RREp/q/e1fkBhaT
IXsMbQLVZ6WSHza+VDL89bCENTJirq9m73MS60Y3653CRypiaQSbHGRr1YiFeM4i
IvW3b6UB1XaeKqy8FZSThRuWwoqykbk3gtB0aGev9Uu9TcwcKSrIHKFy5nej6KlB
YoeqqYaFoDesqOT0KPx2x7M2la3ExsHbEAWls6E4oUQ3gvZc7kUjeLPt4yb4ex9B
kZRbIvbLitiiWkF8PQa0Rg9+IDyTGI2k0U1VMgwCNG1tb7DdBgLSsU36Ad1UQiBG
z+A1LTj2CPXtK8NiQLfKFHeIL2kVk0mpDjLgTy7LKyWhWkNAh1a7L7qFyfIEhz7u
KRV9DLzy8Fw6b8hVg2fQfLuz8CHN/SbatVgIEv4uvsTUM6O3Y6fic4IfFbjCINYs
s1foySIctVncqCl5eqd3IOQ48xdPRd7PTn/96x6VT56EsK7yjhvzQ9pf00MhDlfS
bkrajtu8DmbnQHeNhqCV/HZ/T/6/jD7TjwRRStSfI5tvGiuyqbVzfOIFfyB+pxB3
/bwHAx8v0WkBPXyGLqbnU1sAIYEnR+YuEHD5aV6UHc0g+zwvblBHYgh2ns3uFFnP
TdEMPqB/d2zdjKsbFGP/Uqn01lQikALOFihe/gt7AdfoH7ND9v0Q7jrRyX0kFUEq
5X0CAwEAAaBHMEUGCSqGSIb3DQEJDjE4MDYwCQYDVR0TBAIwADALBgNVHQ8EBAMC
BaAwHAYDVR0RBBUwE4IRaWNjZXJ0Lm5oaS5nb3YudHcwDQYJKoZIhvcNAQELBQAD
ggIBAEWsekXqZZ3oLKUfs/WA71gqSD1+cMfL34PNxPbgssJE0t8mj3S9V2NMKDAe
BO/AYY+SjqwmQ8ewhgTAMNH73bDYwUP0qtoyJuh2f1cSDDW9Y8Cm1AqkI1H525AA
eLcNnYoOTZnczVkYROgiK5Sw9iNymxBzuhOK7w09wNRERU1AhtOjrT5cgGCCcxjn
f6sHnvOGThrwi1WWqKT0phA4XH5eLsIFza+etmQLFvkpKeuVkqG4dXeBUgSISrii
yle+TMySa62CfmBmYTIFA0HRSJbEG2C7eUniN6SlAJnfRMTx1uMN4JhOnhCD5Muq
ZhYHX664K6wR9zwu88JS7ob8VSKZoRUJ6v4elBLNCvQTrGKmtBV60LSPmXOt1Git
6H06MaS4DNnrhiPbpkG4uRnPKUy2ZpNZyyokp6VWdipwXy/1KqN7yLw0bqopI8pL
NSsfWw1S6GhwGtxBTXSjNEIzuuLHQL++HQfoSntp2KXMSrSug7xjejrVUUs9a3hR
cZSpZfEXIQwXLr3Gg5ggtcM7QzAxAtaEf28P9eAfTPUhZhO/w6yXJueAs2xaSqIb
+etJpcQG2nr34IwB4HmXZn6yyLx6cShaomzwneFV31rFozpcjhb/Lrz6I7VICDed
MAAKm37arxKjmtKBCunP1OblCzv4rjJ1BqAcnVWIFkthhqvq
-----END CERTIFICATE REQUEST-----

51
certs/host.key Normal file
View File

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

14
certs/san.cnf Normal file
View File

@ -0,0 +1,14 @@
[ req ]
distinguished_name = req_dn
req_extensions = req_ext
prompt = no
[ req_dn ]
C = TW
ST = Taiwan
O = Inndy's NHI Smartcard Client
[ req_ext ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = iccert.nhi.gov.tw

10
certs/trust_ca.cmd Normal file
View File

@ -0,0 +1,10 @@
@echo off
net session >nul 2>&1
if /I %errorLevel% NEQ 0 (
echo Administrator privilege required
exit
)
certutil.exe -addstore root ca.crt
pause

3
certs/trust_ca_macos.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
security add-trusted-cert -r trustRoot -k ~/Library/Keychains/login.keychain-db ca.crt

View File

@ -0,0 +1,14 @@
#!/bin/bash
if [ ! -x "$(which certutil 2>&-)" ]
then
echo "[-] Install libnss3-tools first"
fi
for f in ~/.mozilla/firefox/*.default*/cert9.db
do
echo --------------------------------------------------------------------------------
echo $f
certutil -d "${f%/*}" -A -i ca.crt -n 'Inndys NHI Smartcard Client' -t C
certutil -d "${f%/*}" -L
done

191
complicated_sam_hc_auth.py Normal file
View File

@ -0,0 +1,191 @@
# This file is part of twnhi-smartcard-agent.
#
# twnhi-smartcard-agent is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# twnhi-smartcard-agent is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with twnhi-smartcard-agent.
# If not, see <https://www.gnu.org/licenses/>.
import os
import socket
from hexdump import hexdump
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptos import DES3, pkcs5_pad, pkcs5_unpad, L_KEY
from errors import ServiceError
DEBUG = bool(os.getenv('DEBUG_MODE', None))
DEFAULT_HOST = os.getenv('NIC_SMARTCARD_AUTH_HOST', 'cloudicap.nhi.gov.tw')
DEFAULT_PORT = int(os.getenv('NIC_SMARTCARD_AUTH_HOST', 443))
def recvall(conn, err_code, err_desc):
data = b''
while not data.endswith(b'<E>'):
try:
data += conn.recv(4096)
except Exception as e:
raise ServiceError(err_code, err_desc, e)
return data
def send_packet(conn, data, error_code, description):
try:
conn.sendall(data)
except Exception as e:
raise ServiceError(error_code, description, e)
def encrypt(key, data):
cipher = DES3.new(key, DES3.MODE_ECB)
encrypted = cipher.encrypt(pkcs5_pad(data))
return encrypted + b'<E>'
def decrypt(key, data):
# funciton `recvall` already ensures that data will end with b'<E>'
assert data.endswith(b'<E>')
cipher = DES3.new(key, DES3.MODE_ECB)
decrypted = cipher.decrypt(data[:-3])
return pkcs5_unpad(decrypted)
def debug_dump(name, data):
if DEBUG:
print('%s:' % name)
hexdump(data)
def handshake(conn):
try:
# generate rsa key for handshake
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=1024,
backend=default_backend()
)
except Exception as e:
raise ServiceError(8300, 'Failed to generate RSA key', e)
# hello packet, send our public key
try:
pubkey = private_key.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).rstrip(b'\n')
except Exception as e:
raise ServiceError(8301, 'Failed to dump public key', e)
data = b'Hello %s' % pubkey
debug_dump('Local hello', data)
send_packet(conn, encrypt(L_KEY, data), 8306, 'Failed to send handshake packet')
# recv remote hello
packet = recvall(conn, 8305, 'Recv error')
try:
data = decrypt(L_KEY, packet)
debug_dump('Remote hello', data)
assert data[:5] == b'Hello'
except Exception as e:
raise ServiceError(8305, 'Decrypt error', e)
# decrypt remote nonce
try:
remote_nonce = private_key.decrypt(data[0x11b:0x11b+0x80], padding.PKCS1v15())
except Exception as e:
raise ServiceError(8305, 'RSA decrypt error', e)
# prepare and encrypt our nonce with remote public key
nonce = os.urandom(16)
try:
remote_public = serialization.load_pem_public_key(
data[6:0x116],
backend=default_backend()
)
enc_nonce = remote_public.encrypt(nonce, padding.PKCS1v15())
except Exception as e:
raise ServiceError(8303, 'Pubkey encrypt failed', e)
send_packet(conn, b' %d %s<E>' % (len(enc_nonce), enc_nonce), 8304, \
'Failed to send nonce')
# concat nonces to make session key
return (nonce + remote_nonce)[:24]
def connect(host=DEFAULT_HOST, port=DEFAULT_PORT):
try:
return socket.create_connection((host, port))
except Exception as e:
raise ServiceError(4061, 'Can not connect to host', e)
def sam_hc_auth_check(raise_on_failed=False):
with connect() as conn:
sess_key = handshake(conn)
send_packet(conn, b'77<E>', 8003, 'Failed to send test packet')
ret = recvall(conn, 8005, 'Service check failed') == b'04<rc=2>OK<E>'
if not ret and raise_on_failed:
raise ServiceError(8005, 'Service check failed')
return ret
def sam_hc_auth(client, to_sign):
with connect() as conn:
sess_key = handshake(conn)
# prepare data to be signed
client.select_applet()
hcid = client.get_hc_card_id()
rnd = client.get_random()
# send auth request
assert len(hcid) == 12 and len(rnd) == 8
data = b'01<id=12>%s<rn=8>%s<E>' % (hcid, rnd)
packet = encrypt(sess_key, data)
send_packet(conn, packet, 8003, 'Failed to send auth request 01')
# recv challenge
packet = recvall(conn, 8005, 'Failed to recv challenge')
data = decrypt(sess_key, packet)
debug_dump('Challenge', data)
# b'02<au=32>................................<E>'
if not (data.startswith(b'02<au=32>') and data.endswith(b'<E>')):
raise ServiceError(8005, 'Failed to decrypt challenge')
challenge = data[9:9+32]
# use hccard to sign challenge
response = client.muauth_hc_dc_sam(challenge)
debug_dump('Response', response)
if len(response) != 16:
raise ServiceError(8006, 'Invalid data length from SAM signing')
# send challenge and data to be signed
if len(to_sign) != 20:
raise ServiceError(8006, 'Invalid data length `to_sign`')
data = b'03<au=16>%s<se=20>%s<E>' % (response, to_sign)
packet = encrypt(sess_key, data)
send_packet(conn, packet, 8007, 'Failed to send response')
# got signature
data = decrypt(sess_key, recvall(conn, 8008, 'Failed to recv signature'))
debug_dump('Signature', data)
# b'04<rc=2>OK<si=256>' ...(256bytes) b'<E>'
if not (data.startswith(b'04<rc=2>OK<si=256>') and data.endswith(b'<E>')):
raise ServiceError(8008, 'Failed to decrypt signature')
return data[18:-3]
if __name__ == '__main__':
sam_hc_auth_check()
from hccard import HealthInsuranceSmartcardClient
with HealthInsuranceSmartcardClient() as client:
sig = sam_hc_auth(client, b'00011234123412341234')
print('sig = %r' % sig)

121
cryptos.py Normal file
View File

@ -0,0 +1,121 @@
# This file is part of twnhi-smartcard-agent.
#
# twnhi-smartcard-agent is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# twnhi-smartcard-agent is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with twnhi-smartcard-agent.
# If not, see <https://www.gnu.org/licenses/>.
import os
import datetime
import hashlib
from Cryptodome.Cipher import DES, DES3
bKEY = b'12345678123456780' * 10
K_BOX = [
0x56, 0x28, 0x34, 0x2E, 0x78, 0x5, 0xF, 0x5A,
0x36, 0x44, 0x42, 0x19, 0x26, 0x95, 0x26, 0x4D,
0x3, 0x10, 0x15, 0x58, 0x3, 0x40, 0x5A, 0x72,
0x1E, 0xB, 0x49, 0x69, 0x4B, 0x15, 0x29, 0x6
]
K_BOX1 = [
0x56, 0x28, 0x34, 0x2E, 0x78, 0x5, 0xF, 0x5A,
0x36, 0x44, 0x42, 0x19, 0x26, 0x95, 0x26, 0x4D,
0x2D, 0x41, 0x4D, 0x1F, 0x41, 0x62, 0x15, 0x2F
]
L_KEY = bytes(bKEY[K_BOX[i]] for i in range(16)) + b'\0' * 8
L_KEY1 = bytes(bKEY[K_BOX1[i]] for i in range(24))
KEY_SUFFIX = b'\x27\x06\x58\x66'
TDesLKey = DES3.new(L_KEY, DES3.MODE_ECB)
TDesLKey1 = DES3.new(L_KEY1, DES3.MODE_ECB)
def iv_pad(d):
def rand_byte():
return os.urandom(1)
bcount = len(d) // 7
if len(d) % 7:
bcount += 1
blocks = [ d[i*7:i*7 + 7].ljust(7, b'\0') + rand_byte() for i in range(bcount) ]
return b''.join(blocks)
def iv_remove(d, flag=True):
c = b''.join(d[i*8:i*8+7] for i in range(len(d) // 8))
if flag:
unpad_size = (len(c) // 8) * 8
return c[:unpad_size]
return c
def pkcs5_tail(n):
return bytes([n]) * n
def pkcs5_pad(data):
padding_size = 8 - len(data) % 8
return data + pkcs5_tail(padding_size)
def pkcs5_unpad(data):
last_byte = data[-1]
tail = data[-last_byte:]
if last_byte > 8 or bytes(tail) != pkcs5_tail(last_byte):
raise ValueError('Inalid PKCS5 padding')
return data[:-last_byte]
def card_encrypt(data, cardid):
t = datetime.date.today().strftime('%Y%m%d')
tdeskey = hashlib.sha1((cardid + t).encode('ascii')).digest() + KEY_SUFFIX
cipher = DES3.new(tdeskey, DES3.MODE_ECB)
data = cipher.encrypt(pkcs5_pad(data))
return TDesLKey1.encrypt(iv_pad(data))
def card_decrypt(data, cardid):
t = datetime.date.today().strftime('%Y%m%d')
tdeskey = hashlib.sha1((cardid + t).encode('ascii')).digest() + KEY_SUFFIX
cipher = DES3.new(tdeskey, DES3.MODE_ECB)
data = TDesLKey1.decrypt(data)
data = iv_remove(data)
data = cipher.decrypt(data)
return pkcs5_unpad(data)
def basic_encrypt(data):
key = datetime.date.today().strftime('%m%d%Y').encode('ascii')
cipher = DES.new(key, DES.MODE_ECB)
return cipher.encrypt(iv_pad(data))
def basic_decrypt(data):
key = datetime.date.today().strftime('%m%d%Y').encode('ascii')
cipher = DES.new(key, DES.MODE_ECB)
decrypted = cipher.decrypt(data)
return iv_remove(decrypted, False)
if __name__ == '__main__':
data, card_id = b'123456123456', '000000000001'
if card_decrypt(card_encrypt(data, card_id), card_id) == data:
print('card_* : Pass')
else:
print('card_* : Failed')
for i in range(40, 40+2*6):
test_data = bytes(range(i))
if basic_decrypt(basic_encrypt(test_data + b'\xff')).split(b'\xff')[0] != test_data:
print('basic_* : Failed')
break
else:
print('basic_* : Pass')
import sys
if len(sys.argv) > 1:
data = bytes.fromhex(sys.argv[1])
print(basic_decrypt(data).decode('big5-hkscs'))

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -0,0 +1,23 @@
# Chrome FoxyProxy 使用說明 / Chrome FoxyProxy Usage
## 新增 Proxy / Config new proxy
### 開啟 FoxyProxy 設定 / Open "options" of FoxyProxy
![開啟 FoxyProxy 設定 / Open "options" of FoxyProxy](chrome-foxyproxy-01.png)
### 按下 "Add New Proxy" / Click "Add New Proxy"
![按下 "Add New Proxy" / Click "Add New Proxy"](chrome-foxyproxy-02.png)
### 填寫 Proxy 資料並且按下 "Save" / Fill proxy config and click "Save"
![填寫 Proxy 資料並且按下 "Save" / Fill proxy config and click "Save"](chrome-foxyproxy-03.png)
## 啟用 Proxy / Enable Proxy
![啟用 Proxy / Enable Proxy](chrome-foxyproxy-04.png)
## 停用 Proxy / Disable Proxy
![停用 Proxy / Disable Proxy](chrome-foxyproxy-04.png)

21
errors.py Normal file
View File

@ -0,0 +1,21 @@
# This file is part of twnhi-smartcard-agent.
#
# twnhi-smartcard-agent is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# twnhi-smartcard-agent is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with twnhi-smartcard-agent.
# If not, see <https://www.gnu.org/licenses/>.
class ServiceError(Exception):
def __init__(self, error_code, description, *args):
super().__init__(*args)
self.error_code = error_code
self.description = description

170
hccard.py Normal file
View File

@ -0,0 +1,170 @@
# This file is part of twnhi-smartcard-agent.
#
# twnhi-smartcard-agent is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# twnhi-smartcard-agent is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with twnhi-smartcard-agent.
# If not, see <https://www.gnu.org/licenses/>.
#!/usr/bin/env python3
import logging
import sys
from collections import namedtuple
from smartcard.System import readers as get_readers
from smartcard.util import toHexString
logging.basicConfig(level='INFO', stream=sys.stdout)
logger = logging.getLogger(__name__)
class SmartcardException(Exception):
pass
class SmartcardCommandException(SmartcardException):
def __init__(self, *args):
super.__init__(*args)
self.error_code = None
self.description = None
class SmartcardClient:
def __init__(self, conn=None):
if conn is None:
conn = select_reader_and_connect()
if not conn:
raise SmartcardException('Smartcard connection was not provided')
self.conn = conn
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
def close(self):
if self.conn:
self.conn.disconnect()
def fire(self, cmd):
data, a, b = self.conn.transmit(cmd)
if (a, b) != (0x90, 0x00):
raise SmartcardCommandException(data, (a, b))
return bytes(data)
HCBasicData = namedtuple('HCBaseData', ['card_id', 'id', 'name', 'birth', 'gender', 'unknown'])
def error_info(error_code, description):
def error_wrapper(f):
def wrapper(*args, **kwargs):
try:
return f(*args, **kwargs)
except SmartcardCommandException as e:
e.error_code = error_code
e.description = description
raise
return wrapper
return error_wrapper
class HealthInsuranceSmartcardClient(SmartcardClient):
@error_info(7004, 'Failed to select applet')
def select_applet(self):
logger.debug('select default applet')
self.fire([
0x00, 0xA4, 0x04, 0x00, 0x10, 0xD1, 0x58, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x11, 0x00
])
def select_sam_applet(self):
logger.debug('select sam applet')
self.fire([
0x00, 0xA4, 0x04, 0x00, 0x10, 0xD1, 0x58, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x31, 0x00
])
@error_info(8011, 'Failed to get basic data')
def get_basic(self):
logger.debug('get basic data')
data = self.fire([0, 0xca, 0x11, 0, 2, 0, 0, 0])
return HCBasicData(
data[:12].decode('ascii'),
data[32:42].decode('ascii'),
data[12:32].rstrip(b'\0').decode('big5-hkscs'),
data[42:49].decode('ascii'),
data[49:50].decode('ascii'),
data[50:].decode('ascii'),
)
@error_info(8010, 'Failed to get card data')
def get_hc_card_data(self):
logger.debug('get HC card data')
return self.fire([0, 0xca, 0x24, 0, 2, 0, 0, 0])
@error_info(8001, 'Failed to get card id')
def get_hc_card_id(self):
logger.debug('get HC card id')
return self.fire([0, 0xca, 0, 0, 2, 0, 0, 0])
@error_info(8002, 'Failed to get card random')
def get_random(self):
logger.debug('get random')
return self.fire([0, 0x84, 0, 0, 8])
@error_info(8006, 'Secure access module signing failed')
def muauth_hc_dc_sam(self, data: bytes):
logger.debug('muauth_hc_dc_sam')
if len(data) > 32:
raise ValueError('data size must be less than 33 bytes')
prefix = [0x00, 0x82, 0x11, 0x12, 0x20]
suffix = [0x10]
payload = prefix + list(data.ljust(32, b'\0')) + suffix
assert len(payload) == 0x26
return self.fire(payload)
def select_reader_and_connect(interactive=False):
readers = get_readers()
if not readers:
logger.error('Please connect your smartcard reader')
return
elif len(readers) == 1:
logger.info('Only one reader connected, use that one: %s', readers[0])
reader = readers[0]
elif not interactive:
logger.info('Non-interactive was used, select first reader')
reader = readers[0]
else:
print('%d readers available, please select one:' % len(readers))
for i, r in enumerate(readers):
print('%-2d : %s' % (i, r))
idx = int(input('\n Reader number: '))
reader = readers[idx]
conn = reader.createConnection()
conn.connect()
return conn
if __name__ == '__main__':
try:
conn = select_reader_and_connect(True)
if not conn:
raise Exception('No reader connected or selection failed')
except Exception as e:
logger.exception('Can not connect to reader, error: %r', e)
sys.exit(1)
with HealthInsuranceSmartcardClient(conn) as client:
client.select_applet()
print(client.get_basic())

144
install-packages.py Normal file
View File

@ -0,0 +1,144 @@
# This file is part of twnhi-smartcard-agent.
#
# twnhi-smartcard-agent is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# twnhi-smartcard-agent is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with twnhi-smartcard-agent.
# If not, see <https://www.gnu.org/licenses/>.
import os
import sys
from hashlib import sha256
from imp import reload
from io import BytesIO
from pprint import pprint
from urllib.request import urlopen
from zipfile import ZipFile
SWIG_LOCAL_FILENAME = 'swigwin-4.0.1.zip'
SIWG_ZIP_URL = 'http://prdownloads.sourceforge.net/swig/swigwin-4.0.1.zip'
SWIG_ZIP_HASH = '8c504241ad4fb4f8ba7828deaef1ea0b4972e86eb128b46cb75efabf19ab4745'
is_windows = os.name == 'nt'
def pyexec(*args, executable=sys.executable):
return os.system('%s -m %s' % (executable, ' '.join(args)))
def which(fname):
if is_windows:
fname += '.exe'
for p in os.getenv('PATH').split(os.path.pathsep):
full = os.path.join(p, fname)
if os.path.exists(full):
return full
def check_version():
if sys.version_info.major < 3 or \
sys.version_info.minor < 6:
print('[-] Python version not match: %s' % sys.version)
exit()
def install_virtualenv():
try:
import virtualenv
major_version = int(virtualenv.__version__.split('.')[0])
if major_version >= 20:
return
else:
print('[*] Upgrade virtualenv')
except:
pass
ret = pyexec('pip', 'install', '-U', '--user', 'virtualenv')
if ret:
print('[-] Failed to execute pip')
exit(1)
def load_virtualenv():
if not os.path.exists('venv'):
print('[*] Create new virtualenv')
pyexec('virtualenv', '--copies', '--download', 'venv')
print('[*] Activate venv in current interpreter')
the_file = os.path.join('venv', 'Scripts', 'activate_this.py') \
if is_windows else \
os.path.join('venv', 'bin', 'activate_this.py')
exec(open(the_file).read(), {'__file__': the_file})
def load_swig():
if not is_windows:
return
if which('swig'):
return
if not os.path.exists(SWIG_LOCAL_FILENAME):
print('[+] Downloading file from %s' % SIWG_ZIP_URL)
response = urlopen(SIWG_ZIP_URL)
data = response.read()
with open(SWIG_LOCAL_FILENAME, 'wb') as fp:
fp.write(data)
else:
print('[+] Use %s from local' % SWIG_LOCAL_FILENAME)
with open(SWIG_LOCAL_FILENAME, 'rb') as fp:
data = fp.read()
print('[*] Check if file hash match %s' % SWIG_ZIP_HASH)
assert sha256(data).hexdigest().lower() == SWIG_ZIP_HASH
print('[*] Read zip file')
zfile = ZipFile(BytesIO(data))
pathname = zfile.infolist()[0].filename
if os.path.exists(pathname):
print('[+] Zip file already extracted')
else:
print('[+] Extracting files')
zfile.extractall('.')
path = os.getenv('PATH')
swig_path = os.path.join(os.path.abspath('.'), 'swigwin-4.0.1')
new_path = swig_path + os.path.pathsep + path
os.putenv('PATH', new_path)
print('New $PATH:')
pprint(new_path.split(os.path.pathsep))
def install_dependencies():
print('[*] Installing dependencies')
ret = pyexec('pip', 'install', '-r', 'requirements.txt', executable='python')
if ret:
print('[-] Failed to install dependencies')
exit(1)
def try_import_packages():
try:
import hexdump
import websockets
import Cryptodome
import smartcard
except ImportError as e:
print('[-] Can not import one of dependencies: %s' % e.name)
exit(1)
def finish():
print('[!] We are good to go!')
print('[*] Follow post-installation instructions to setup root certiciate and run the program')
if __name__ == '__main__':
check_version()
install_virtualenv()
load_virtualenv()
load_swig()
install_dependencies()
try_import_packages()
finish()

357
pysoxy.py Normal file
View File

@ -0,0 +1,357 @@
# This file is part of twnhi-smartcard-agent.
#
# twnhi-smartcard-agent is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# twnhi-smartcard-agent is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with twnhi-smartcard-agent.
# If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
"""
Small Socks5 Proxy Server in Python
from https://github.com/MisterDaneel/
"""
# Network
import socket
import select
from struct import pack, unpack
# System
import traceback
from threading import Thread, activeCount
from signal import signal, SIGINT, SIGTERM
from time import sleep
import sys
hijacker = None
hijacked_host = None
#
# Configuration
#
MAX_THREADS = 200
BUFSIZE = 2048
TIMEOUT_SOCKET = 5
LOCAL_ADDR = '127.0.0.1'
LOCAL_PORT = 17777
# Parameter to bind a socket to a device, using SO_BINDTODEVICE
# Only root can set this option
# If the name is an empty string or None, the interface is chosen when
# a routing decision is made
# OUTGOING_INTERFACE = "eth0"
OUTGOING_INTERFACE = ""
#
# Constants
#
'''Version of the protocol'''
# PROTOCOL VERSION 5
VER = b'\x05'
'''Method constants'''
# '00' NO AUTHENTICATION REQUIRED
M_NOAUTH = b'\x00'
# 'FF' NO ACCEPTABLE METHODS
M_NOTAVAILABLE = b'\xff'
'''Command constants'''
# CONNECT '01'
CMD_CONNECT = b'\x01'
'''Address type constants'''
# IP V4 address '01'
ATYP_IPV4 = b'\x01'
# DOMAINNAME '03'
ATYP_DOMAINNAME = b'\x03'
class ExitStatus:
""" Manage exit status """
def __init__(self):
self.exit = False
def set_status(self, status):
""" set exist status """
self.exit = status
def get_status(self):
""" get exit status """
return self.exit
def error(msg="", err=None):
""" Print exception stack trace python """
if msg:
traceback.print_exc()
print("[-] {} - Code: {}, Message: {}".format(msg, str(err[0]), err[1]))
else:
traceback.print_exc()
def proxy_loop(socket_src, socket_dst):
""" Wait for network activity """
while not EXIT.get_status():
try:
reader, _, _ = select.select([socket_src, socket_dst], [], [], 1)
except select.error as err:
error("Select failed", err)
return
if not reader:
continue
try:
for sock in reader:
data = sock.recv(BUFSIZE)
if not data:
return
if sock is socket_dst:
socket_src.send(data)
else:
socket_dst.send(data)
except socket.error as err:
error("Loop failed", err)
return
def connect_to_dst(dst_addr, dst_port):
""" Connect to desired destination """
sock = create_socket()
if OUTGOING_INTERFACE:
try:
sock.setsockopt(
socket.SOL_SOCKET,
socket.SO_BINDTODEVICE,
OUTGOING_INTERFACE.encode(),
)
except PermissionError as err:
print("[-] Only root can set OUTGOING_INTERFACE parameter")
EXIT.set_status(True)
try:
sock.connect((dst_addr, dst_port))
print('[+] Connect to %s:%d' % (dst_addr, dst_port))
return sock
except socket.error as err:
error("Failed to connect to DST", err)
return 0
def request_client(wrapper):
""" Client request details """
# +----+-----+-------+------+----------+----------+
# |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
# +----+-----+-------+------+----------+----------+
try:
s5_request = wrapper.recv(BUFSIZE)
except ConnectionResetError:
if wrapper != 0:
wrapper.close()
error()
return False
# Check VER, CMD and RSV
if (
s5_request[0:1] != VER or
s5_request[1:2] != CMD_CONNECT or
s5_request[2:3] != b'\x00'
):
return False
# IPV4
if s5_request[3:4] == ATYP_IPV4:
dst_addr = socket.inet_ntoa(s5_request[4:-2])
dst_port = unpack('>H', s5_request[8:len(s5_request)])[0]
# DOMAIN NAME
elif s5_request[3:4] == ATYP_DOMAINNAME:
sz_domain_name = s5_request[4]
dst_addr = s5_request[5: 5 + sz_domain_name - len(s5_request)]
port_to_unpack = s5_request[5 + sz_domain_name:len(s5_request)]
dst_port = unpack('>H', port_to_unpack)[0]
else:
return False
return (dst_addr, dst_port)
def request(wrapper):
"""
The SOCKS request information is sent by the client as soon as it has
established a connection to the SOCKS server, and completed the
authentication negotiations. The server evaluates the request, and
returns a reply
"""
dst = request_client(wrapper)
# Server Reply
# +----+-----+-------+------+----------+----------+
# |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
# +----+-----+-------+------+----------+----------+
rep = b'\x07'
bnd = b'\x00' + b'\x00' + b'\x00' + b'\x00' + b'\x00' + b'\x00'
hijacked = False
if dst:
if dst[0] == hijacked_host.encode():
print('[*] Hijack %s to local server' % hijacked_host)
hijacked = True
socket_dst = True
else:
socket_dst = connect_to_dst(dst[0], dst[1])
if not dst or socket_dst == 0:
rep = b'\x01'
else:
rep = b'\x00'
if hijacked:
bnd = b'\x01\x01\x01\x01\x01\x01'
else:
bnd = socket.inet_aton(socket_dst.getsockname()[0])
bnd += pack(">H", socket_dst.getsockname()[1])
reply = VER + rep + b'\x00' + ATYP_IPV4 + bnd
try:
wrapper.sendall(reply)
except socket.error:
if wrapper != 0:
wrapper.close()
return
# start proxy
if rep == b'\x00':
if hijacked:
hijacker(wrapper)
else:
proxy_loop(wrapper, socket_dst)
if wrapper != 0:
wrapper.close()
if socket_dst != 0 and socket_dst != True:
socket_dst.close()
def subnegotiation_client(wrapper):
"""
The client connects to the server, and sends a version
identifier/method selection message
"""
# Client Version identifier/method selection message
# +----+----------+----------+
# |VER | NMETHODS | METHODS |
# +----+----------+----------+
try:
identification_packet = wrapper.recv(BUFSIZE)
except socket.error:
error()
return M_NOTAVAILABLE
# VER field
if VER != identification_packet[0:1]:
return M_NOTAVAILABLE
# METHODS fields
nmethods = identification_packet[1]
methods = identification_packet[2:]
if len(methods) != nmethods:
return M_NOTAVAILABLE
for method in methods:
if method == ord(M_NOAUTH):
return M_NOAUTH
return M_NOTAVAILABLE
def subnegotiation(wrapper):
"""
The client connects to the server, and sends a version
identifier/method selection message
The server selects from one of the methods given in METHODS, and
sends a METHOD selection message
"""
method = subnegotiation_client(wrapper)
# Server Method selection message
# +----+--------+
# |VER | METHOD |
# +----+--------+
if method != M_NOAUTH:
return False
reply = VER + method
try:
wrapper.sendall(reply)
except socket.error:
error()
return False
return True
def connection(wrapper):
""" Function run by a thread """
if subnegotiation(wrapper):
request(wrapper)
def create_socket():
""" Create an INET, STREAMing socket """
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(TIMEOUT_SOCKET)
except socket.error as err:
error("Failed to create socket", err)
sys.exit(0)
return sock
def bind_port(sock):
"""
Bind the socket to address and
listen for connections made to the socket
"""
try:
print('[+] Socks5 proxy bind on {}:{}'.format(LOCAL_ADDR, str(LOCAL_PORT)))
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((LOCAL_ADDR, LOCAL_PORT))
except socket.error as err:
error("Bind failed", err)
sock.close()
sys.exit(0)
# Listen
try:
sock.listen(10)
except socket.error as err:
error("Listen failed", err)
sock.close()
sys.exit(0)
return sock
def exit_handler(signum, frame):
""" Signal handler called with signal, exit script """
print('[*] Signal handler called with signal', signum)
EXIT.set_status(True)
def main(hijack, host):
""" Main function """
global hijacker
global hijacked_host
hijacker = hijack
hijacked_host = host
new_socket = create_socket()
bind_port(new_socket)
#signal(SIGINT, exit_handler)
#signal(SIGTERM, exit_handler)
while not EXIT.get_status():
if activeCount() > MAX_THREADS:
sleep(3)
continue
try:
wrapper, _ = new_socket.accept()
wrapper.setblocking(1)
except socket.timeout:
continue
except socket.error:
error()
continue
except TypeError:
error()
sys.exit(0)
recv_thread = Thread(target=connection, args=(wrapper, ))
recv_thread.start()
new_socket.close()
EXIT = ExitStatus()
if __name__ == '__main__':
main()

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
cryptography
hexdump
pycryptodomex
pyscard
websockets

199
server.py Normal file
View File

@ -0,0 +1,199 @@
# This file is part of twnhi-smartcard-agent.
#
# twnhi-smartcard-agent is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# twnhi-smartcard-agent is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with twnhi-smartcard-agent.
# If not, see <https://www.gnu.org/licenses/>.
#!/usr/bin/env python3
import asyncio
import atexit
import contextlib
import http
import logging
import os
import ssl
import subprocess
import sys
import threading
import websockets
from hccard import HealthInsuranceSmartcardClient, select_reader_and_connect, \
SmartcardCommandException
from cryptos import card_encrypt, basic_encrypt
from complicated_sam_hc_auth import sam_hc_auth, sam_hc_auth_check
from errors import ServiceError
logging.basicConfig(level='INFO', stream=sys.stdout)
logger = logging.getLogger('server')
HOST = 'iccert.nhi.gov.tw'
CENSORED_COMMANDS = ['EnCrypt', 'SecureGetBasicWithParam', 'GetBasic']
lock = threading.Lock()
class HTTP(websockets.WebSocketServerProtocol):
async def process_request(self, path, request_headers):
if path == '/echo':
return await super().process_request(path, request_headers)
elif path == '/exit':
exit()
elif path == '/':
body = b'It works!\n'
return http.HTTPStatus.OK, [('Content-Length', str(len(body)))], body
else:
return http.HTTPStatus.NOT_FOUND, [], b''
@staticmethod
def process_origin(headers, origins):
origin = websockets.WebSocketServerProtocol.process_origin(headers, origins)
if origin:
print('[*] wss connection from: %s' % origin)
if not origin or not origin.endswith('.gov.tw') and \
not origin.endswith('iccert.nhi.gov.tw:7777'):
raise websockets.InvalidOrigin(origin)
return origin
def connect_reader():
try:
return HealthInsuranceSmartcardClient()
except:
raise ServiceError(8013, 'Can not connect to smartcard reader')
def get_basic_data():
with lock, connect_reader() as client:
try:
client.select_applet()
data = list(client.get_basic()[:-1])
data.append(client.get_hc_card_data().decode('ascii')[:1])
return ','.join(data)
except SmartcardCommandException as e:
raise
except:
raise ServiceError(8011, 'Failed to read basic data from smartcard')
def get_basic_data_encrypted(password):
# Yes, password was not used to encrypt the data!
# maybe we should remove the password argument and rename it to encoded?
blob = get_basic_data().encode('big5-hkscs')
return basic_encrypt(blob).hex().upper()
async def handler(ws, path):
try:
while True:
cmd = await ws.recv()
log_censored = any(cmd.startswith(c) for c in CENSORED_COMMANDS)
def censor_data(data, splitter='='):
if log_censored:
data_list = data.split(splitter, maxsplit=1)
if len(data_list) == 1:
return data
else:
return data_list[0] + splitter + '...(censored)'
return data
logger.info('InCmd = {{{ %s }}}' % censor_data(cmd))
prefix = ''
try:
if cmd == 'Exit':
exit()
elif cmd == 'GetVersion':
ret = 'GetVersion:0001'
elif cmd == 'GetBasic':
prefix = 'GetBasic:'
ret = get_basic_data()
elif cmd == 'GetRandom':
rnd = int.from_bytes(os.urandom(8), 'little')
ret = str(rnd).zfill(16)[-16:]
assert len(ret) == 16
ret = 'GetRandom:%s' % ret
elif cmd.startswith('EnCrypt?Pwd='):
prefix = 'EnCrypt:'
data = cmd.split('=', maxsplit=1)[1].encode('ascii')
if not (6 <= len(data) <= 12):
raise ServiceError(8009, 'Invalid password length (6 <= len <= 12)')
with lock, connect_reader() as client:
client.select_applet()
card_id = client.get_hc_card_id().decode('ascii')
encrypted = card_encrypt(data, card_id)
ret = encrypted.hex().upper()
elif cmd.startswith('H_Sign?Random='):
prefix = 'H_Sign:'
data = cmd.split('=')[1].encode('ascii')
assert len(data) == 20 and data[:4] == b'0001'
sam_hc_auth_check(raise_on_failed=True)
with lock, connect_reader() as client:
sig = sam_hc_auth(client, data)
ret = sig.decode('ascii')
elif cmd.startswith('SecureGetBasicWithParam?Pwd='):
prefix = 'SecureGetBasicWithParam:'
pwd = cmd.split('=', maxsplit=1)[0]
ret = get_basic_data_encrypted(pwd)
else:
ret = '9999'
except (SmartcardCommandException, ServiceError) as e:
if isinstance(e.error_code, (int, str)):
prefix = ''
result = '%d' % e.error_code
logger.error('Error = {{{ %d: %s }}}' % (e.error_code, e.description))
else:
result = '9876'
logger.error('Error = {{{ Unexpected Error -> %r }}}' % e)
else:
result = prefix + ret
await ws.send(result)
result = censor_data(result, ':')
if len(result) >= 32:
result = '%s...(%d bytes)' % (result[:32], len(result) - 32)
logger.info('OutResult = {{{ %s }}}' % result)
except websockets.ConnectionClosedOK:
pass
except websockets.ConnectionClosedError:
pass
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_context.load_cert_chain('certs/chain.crt', 'certs/host.key')
import pysoxy
class PolyServer:
def is_serving(self):
return True
def forwarder(sock):
# this function will be executed in new thread,
# we need to create a new event loop
event_loop = asyncio.new_event_loop()
server = websockets.WebSocketServer(event_loop)
server.wrap(PolyServer())
_, conn = event_loop.run_until_complete(event_loop.connect_accepted_socket(lambda: HTTP(handler, server, host='localhost', port=7777, secure=True), sock, ssl=ssl_context))
event_loop.run_until_complete(conn.wait_closed())
pysoxy.main(forwarder, HOST)