跳转至

博客

在裸机上部署 ESXi 和 vCSA 7

之前在另一篇文章里提到过 vCSA 的安装,这次又在另一台机器上重新做了一遍,特此记录一下。

首先在官网上下载 ESXi+VCSA 7.0 ,应该得到两个文件:

7.9G VMware-VCSA-all-7.0.1-16860138.iso
358M VMware-VMvisor-Installer-7.0U1-16850804.x86_64.iso

首先安装 ESXi,用 UNetBootin 制作 ESXi 的安装光盘。注意不能用 dd,因为它是 CDFS 格式的,不能直接 boot。启动以后,按照界面要求,一路安装即可。

接着,就可以用网页访问 ESXi 进行配置。比如安装一些 Linux 发行版,然后在 Linux 虚拟机里面 mount 上面的 VCSA 的 iso:

sudo mount /dev/sr0 /mnt

接着,复制并修改 /mnt/vcsa-cli-installer/templates/install/embedded_vCSA_on_ESi.json,按照代码注释进行修改。需要注意几点:

  1. 密码都可以设为空,然后运行 cli 的时候输入
  2. ESXi 的密码和 vCSA 的密码是不一样的
  3. 可以把 ceip 关掉,设置 ceip_enabled: false

接着,进行安装:

/mnt/vcsa-cli-installer/lin64/vcsa-deploy install --accept-eula /path/to/customized.json -v

慢慢等待它安装成功即可。

安装完成后,进入 vCSA,新建一个 Datacenter,然后选择新建的 Datacenter,选择 Add host,输入 ESXi 的地址和用户密码信息即可。

IBM Power S822LC(8335-GTB) BMC 升级

背景

最近拿到一台 IBM Power S822LC(8335-GTB)机器的访问权限,这也是我第一次碰 ppc64le 指令集的服务器。然后发现,它的 BMC 版本比较老,我想连接 Remote Control 失败了,原因是 JViewer 不支持 macOS,我就想着能不能升级一下。

升级过程

首先,在网上找了一下文档,首先用 ipmitool 找一下机器型号:

$ sudo ipmitool fru print 3
 Chassis Type          : Unknown
 Chassis Part Number   : 8335-GTB
 Chassis Serial        : REDACTED

可以看到,这台机器是 8335-GTB 型号,按照这个型号在 Fix Central 上搜索,可以找到若干个版本的 firmware,其中最老的版本是 OP8_v1.11_2.1,对比了一下,和原来的版本一致:

$ sudo ipmitool fru print 47
 Product Name          : OpenPOWER Firmware
 Product Version       : IBM-garrison-ibm-OP8_v1.11_2.1
 Product Extra         :        op-build-da02863
 Product Extra         :        buildroot-81b8d98
 Product Extra         :        skiboot-5.3.6
 Product Extra         :        hostboot-1f6784d-3408af7
 Product Extra         :        linux-4.4.16-openpower1-bc83f92
 Product Extra         :        petitboot-v1.2.4-de6cda2
 Product Extra         :        garrison-xml-3db7b6e
 Product Extra         :        occ-69fb587
 Product Extra         :        hostboot-binar

于是,我下载了比较新的版本,一个 hpm 文件,然后在 BMC 网页上进行升级。第一次升级比较保守,选择了 2017 年的版本:

$ sudo ipmitool fru print 47
Product Name          : OpenPOWER Firmware
 Product Version       : IBM-garrison-ibm-OP8_v1.12_2.72
 Product Extra         :        op-build-14a75d0
 Product Extra         :        buildroot-211bd05
 Product Extra         :        skiboot-5.4.3
 Product Extra         :        hostboot-2eb7706-69b1432
 Product Extra         :        linux-4.4.30-openpower1-084eb48
 Product Extra         :        petitboot-v1.3.2-d709207
 Product Extra         :        garrison-xml-19a5164
 Product Extra         :        occ-d7efe30-47b58cb
 Product Extra         :        hostb

这次升级比较顺利,没有遇到什么障碍。但是我发现,BMC 里面显示的 BIOS 版本和 hpm 对不上,它总是认为 BIOS 版本是落后的,需要更新,而 Firmware 部分(BOOT 和 APP)是更新后的版本。但 BIOS 版本和原来的版本也不一样。于是我重新升级了几次,都没有效果,怀疑是升级出了问题。后来仔细读文档才发现,确实是 BMC 软件的问题(文档):

Note: BMC Dashboard shows an incorrect level for the BIOS caused by improper translation of the level subfields. The Bios number should reflect the PNOR level for the system of "IBM-garrison-ibm-OP8_v1.11_2.19". In this case, the BIOS version should be 1.11_2.19 but shows as 1.17.19 instead with the "11_2" converted into the "17".

The Firmware Revision for the BMC firmware shows correctly as "2.13.58".

Here is an example output of the Dashboard with an errant BIOS Version:

Dashboard gives the overall information about the status of the device and remote server.

Device Information

Firmware Revision: 2.13.58

Firmware Build Time: Oct 26 2016 11:40:55 CDT

BIOS Version: 1.17.19

一番捣鼓之后,不知道怎么了,BMC 就挂了,怎么访问都不通。只好物理断电,重新来过。按照同样的方法,升级到了 2019 年的版本:

 Product Name          : OpenPOWER Firmware
 Product Version       : IBM-garrison-OP8_v1.12_2.96
 Product Extra         :        op-build-v2.3-7-g99a6bc8
 Product Extra         :        buildroot-2019.02.1-16-ge01dcd0
 Product Extra         :        skiboot-v6.3.1
 Product Extra         :        hostboot-p8-c893515-pd6f049d
 Product Extra         :        occ-p8-a2856b7
 Product Extra         :        linux-5.0.7-openpower1-p8e31f00
 Product Extra         :        petitboot-v1.10.3
 Product Extra         :        machine-xml-c5c3

中途也遇到了几次奇怪的问题,多次通过 IPMI reset 之后就好了。

远程访问

但是,最新版的 BMC 固件中,JViewer 依然没有 macOS 支持。我也 SSH 进去确认了一下,确实没有对应的支持文件。只好在 Linux 机器上访问,安装 icedtea 以后,就可以打开 jnlp 文件,之后一切都很正常。

一个可能的替代方案:https://github.com/sciapp/nojava-ipmi-kvm

在 arm64 上使用 rust-analyzer

远程到 arm64 的机器上进行开发,发现没有 rust-analyzer 的支持。研究了一下,发现在 rustup 里面可以找到,不过要配置一下:

> rustup toolchain add nightly
> rustup component add --toolchain nightly rust-analyzer-preview

这个时候,应该可以找到 ~/.rustup/toolchains/nightly-aarch64-unknown-linux-gnu/bin/rust-analyzer 文件,接下来,配置 VSCode 插件即可:

{
    "rust-analyzer.serverPath": "~/.rustup/toolchains/nightly-aarch64-unknown-linux-gnu/bin/rust-analyzer"
}

路径在 ~/.vscode-server/data/Machine/settings.json

参考:https://github.com/rust-analyzer/rust-analyzer/issues/5256

在 Rpi4 上运行 buildroot

背景

需要给 rpi 配置一个 pxe 的最小环境,在上一篇博文了提到可以用 alpine,但发现有一些不好用的地方,所以试了试 buildroot。

PXE 设置和路由器设置

见“在 Rpi4 上运行 Alpine Linux”文章。

Buildroot 配置

下载 buildroot:

> wget https://buildroot.org/downloads/buildroot-2020.08.tar.gz
> unar buildroot-2020.08.tar.gz
> cd buildroot-2020.08
> make raspberrypi4_64_defconfig

然后运行 make menuconfig ,在 Filesystem images 中打开 initramfs,并设置 cpio 压缩为 gz。然后直接编译:

> make -j4
$ ls -al target/images
bcm2711-rpi-4-b.dtb*  boot.vfat  Image  rootfs.cpio  rootfs.cpio.gz  rootfs.ext2  rootfs.ext4@  rpi-firmware/  sdcard.img

接着,在一个单独的目录里,把这些文件整理一下

> cd ~/rpi-buildroot
> cp -r ~/buildroot-2020.08/output/images/rpi-firmware/* .
> cp ~/buildroot-2020.08/output/images/bcm2711-rpi-4-b.dtb .
> cp ~/buildroot-2020.08/output/images/Image .
> cp ~/buildroot-2020.08/output/images/rootfs.cpio.gz .
> # edit cmdline.txt: remove root= and rootwait
> # edit config.txt: uncomment initramfs rootfs.cpio.gz line
# ls
bcm2711-rpi-4-b.dtb*  cmdline.txt  config.txt  fixup.dat  Image  overlays/  rootfs.cpio.gz  start.elf

最后开启 TFTP 服务器即可:

> sudo python3 -m py3tftp -p 69

树莓派启动

连接树莓派的串口,用 115200 Baudrate 打开,可以看到启动信息:

PM_RSTS: 0x00001000
RPi: BOOTLOADER release VERSION:a5e1b95f DATE: Apr 16 2020 TIME: 18:11:29 BOOTMODE: 0x00000006 part: 0 BUILD_TIMESTAMP=1587057086 0xa049cc2f 0x00c03111
uSD voltage 3.3V
... 
Welcome to Buildroot
buildroot login: root
#

默认用户是 root,没有密码。

在 rpi4 上用 PXE 运行 Alpine Linux

背景

需要给 rpi 配置一个 pxe 的最小环境,然后看到 alpine 有 rpi 的支持,所以尝试给 rpi4 配置 alpine。

PXE 设置

第一步是设置 rpi4 的启动模式,打开 BOOT UART 并且打开 网络启动:

> cd /lib/firmware/raspberrypi/bootloader/critical
> rpi-eeprom-config pieeprom-2021-04-29.bin > config.txt
$ cat config.txt
[all]
BOOT_UART=1
WAKE_ON_GPIO=1
POWER_OFF_ON_HALT=0
DHCP_TIMEOUT=45000
DHCP_REQ_TIMEOUT=4000
TFTP_FILE_TIMEOUT=30000
TFTP_IP=
TFTP_PREFIX=0
BOOT_ORDER=0x1
SD_BOOT_MAX_RETRIES=3
NET_BOOT_MAX_RETRIES=5
[none]
FREEZE_VERSION=0
> sed 's/BOOT_UART=0/BOOT_UART=1/;s/BOOT_ORDER=0x1/BOOR_ORDER=0x12/' config.txt > config-pxe.txt
> rpi-eeprom-config --out pieeprom-2021-04-29-pxe.bin --config config-pxe.txt pieeprom-2021-04-29.bin
> rpi-eeprom-update -d -f pieeprom-2021-04-29-pxe.bin
> reboot

重启以后,可以用 vcgencmd bootloader_config 查看当前的启动配置,看是否正确地更新了启动配置。比较重要的是 BOOT_ORDER,0x12 表示先尝试网络启动,再尝试 SD 卡启动。

路由器配置

第二步,需要配置路由器,以 OpenWrt 为例:

> uci add_list dhcp.lan.dhcp_option="66,ip_address_of_tftp_server"
> uci commit dhcp
> /etc/init.d/dnsmasq restart
$ cat /etc/config/dhcp
...
config dhcp 'lan'
        ...
    list dhcp_option '66,ip_address_of_tftp_server'
...

这样就配置完毕了。如果是 isc-dhcp-server,修改 /etc/dhcp/dhcpd.conf

subnet 10.0.1.0 netmask 255.255.255.0 {
    range 10.0.1.100 10.0.1.199;
    option routers 10.0.1.1;
    option tftp-server-name "10.0.1.1";
}

TFTP 服务器配置

下载 alpine linux 的 rpi boot,解压到指定目录:

> wget http://mirrors.tuna.tsinghua.edu.cn/alpine/v3.12/releases/aarch64/alpine-rpi-3.12.0-aarch64.tar.gz
> unar alpine-rpi-3.12.0-aarch64.tar.gz
> cd alpine-rpi-3.12.0-aarch64

修改 cmdline.txt ,把 console=tty1 改成 console=ttyAMA0,115200,并且去掉 quiet;修改 usercfg.txt 为:

dtoverlay=disable-bt
enable_uart=1

接着,启动 TFTP 服务器:

> sudo python3 -m py3tftp -p 69

树莓派启动

连接树莓派的串口,用 115200 Baudrate 打开,可以看到启动信息:

PM_RSTS: 0x00001000
RPi: BOOTLOADER release VERSION:a5e1b95f DATE: Apr 16 2020 TIME: 18:11:29 BOOTMODE: 0x00000006 part: 0 BUILD_TIMESTAMP=1587057086 0xa049cc2f 0x00c03111
uSD voltage 3.3V
... 
initramfs emergency recovery shell launched. Type 'exit' to continue boot
sh: can't access tty; job control turned off
/ #

然后,按照需要自定义 initramfs 即可。解压后,修改文件,然后运行:

> find . -print0 | cpio --null -ov --format=newc | gzip > ../initramfs-rpi4

把自带的 initramfs 替换掉。

用 certbot 申请 route53 上的域名的 LetsEncrypt 证书并上传到 IAM

最近遇到了 AWS Certificate Manager 的一些限制,所以只能用 IAM 证书。于是上网找到了通过 certbot 申请 LE 证书,通过 route53 API 验证的方法。

首先配置 aws 的 credential。然后,按照 certbot:

pip3 install -U certbot certbot_dns_route53

然后,就可以申请证书了:

certbot certonly --dns-route53 --config-dir "./letsencrypt" --work-dir "./letsencrypt" --logs-dir "./letsencrypt"  -d example.com --email a@b.com --agree-tos

如果申请成功,在当前目录下可以找到证书。然后上传到 IAM:

aws iam upload-server-certificate --server-certificate-name NameHere \
    --certificate-body file://letsencrypt/live/example.com/cert.pem \
    --private-key file://letsencrypt/live/example.com/privkey.pem \
    --certificate-chain file://letsencrypt/live/example.com/chain.pem \
    --path /cloudfront/

如果要用于 cloudfront,才需要最后的路径参数;否则可以去掉。这样就完成了 IAM 证书的上传。

在 k8s 中部署 Prometheus

实验了一下在 k8s 中部署 Prometheus,因为它和 k8s 有比较好的集成,很多 App 能在 k8s 里通过 service discovery 被 Prometheus 找到并且抓取数据。实践了一下,其实很简单。

用 helm 进行配置:

helm upgrade --install prometheus stable/prometheus

这样就可以了,如果已经有 StorageClass(比如腾讯云的话,CBS 和 CFS),它就能自己起来了,然后在 Lens 里面也可以看到各种 metrics 的可视化。

如果是自建的单结点的 k8s 集群,那么还需要自己创造 PV,并且把 PVC 绑定上去。我采用的是 local 类型的 PV:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-volume-1
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/srv/k8s-data-1"

---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-volume-2
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/srv/k8s-data-2"

这样,结点上的两个路径分别对应两个 PV,然后只要让 PVC 也用 manual 的 StorageClass 就可以了:

server:
    persistentVolume:
        storageClass: manual

alertmanager:
    persistentVolume:
        storageClass: manual

把这个文件保存为 values.yaml 然后:

helm upgrade --install prometheus stable/prometheus -f values.yaml

这样就可以了。不过 PVC 不能在线改,可能需要删掉重来。

然后,由于权限问题,还需要在结点上修改一下两个目录的权限:

sudo chown -R 65534:65534 /srv/k8s-data-1
sudo chown -R 65534:65534 /srv/k8s-data-2

这样容器内就可以正常访问了。

各种 ecc 曲线

背景知识

椭圆曲线有如下的形式:

第一种:

\[E: y^2 \equiv x^3 + ax + b \mod{p}\]

曲线的参数共有 \((p, a, b, G, n, h)\)\(G\) 是一个点 \((G_x, G_y)\)\(n\)\(G\) 的阶。

第二种:

\[E: y^2+xy=x^3+ax^2+1\]

称为 Kbolitz curve。不同的曲线有不同的参数 \((m,f(x),a,b,G,n,h)\),对应不同的 \(GF(2^m)\) 域。

OpenSSL

看一下 openssl 支持的曲线参数(openssl ecparam -list_curves):

  secp112r1 : SECG/WTLS curve over a 112 bit prime field
  secp112r2 : SECG curve over a 112 bit prime field
  secp128r1 : SECG curve over a 128 bit prime field
  secp128r2 : SECG curve over a 128 bit prime field
  secp160k1 : SECG curve over a 160 bit prime field
  secp160r1 : SECG curve over a 160 bit prime field
  secp160r2 : SECG/WTLS curve over a 160 bit prime field
  secp192k1 : SECG curve over a 192 bit prime field
  secp224k1 : SECG curve over a 224 bit prime field
  secp224r1 : NIST/SECG curve over a 224 bit prime field
  secp256k1 : SECG curve over a 256 bit prime field
  secp384r1 : NIST/SECG curve over a 384 bit prime field
  secp521r1 : NIST/SECG curve over a 521 bit prime field
  prime192v1: NIST/X9.62/SECG curve over a 192 bit prime field
  prime192v2: X9.62 curve over a 192 bit prime field
  prime192v3: X9.62 curve over a 192 bit prime field
  prime239v1: X9.62 curve over a 239 bit prime field
  prime239v2: X9.62 curve over a 239 bit prime field
  prime239v3: X9.62 curve over a 239 bit prime field
  prime256v1: X9.62/SECG curve over a 256 bit prime field
  sect113r1 : SECG curve over a 113 bit binary field
  sect113r2 : SECG curve over a 113 bit binary field
  sect131r1 : SECG/WTLS curve over a 131 bit binary field
  sect131r2 : SECG curve over a 131 bit binary field
  sect163k1 : NIST/SECG/WTLS curve over a 163 bit binary field
  sect163r1 : SECG curve over a 163 bit binary field
  sect163r2 : NIST/SECG curve over a 163 bit binary field
  sect193r1 : SECG curve over a 193 bit binary field
  sect193r2 : SECG curve over a 193 bit binary field
  sect233k1 : NIST/SECG/WTLS curve over a 233 bit binary field
  sect233r1 : NIST/SECG/WTLS curve over a 233 bit binary field
  sect239k1 : SECG curve over a 239 bit binary field
  sect283k1 : NIST/SECG curve over a 283 bit binary field
  sect283r1 : NIST/SECG curve over a 283 bit binary field
  sect409k1 : NIST/SECG curve over a 409 bit binary field
  sect409r1 : NIST/SECG curve over a 409 bit binary field
  sect571k1 : NIST/SECG curve over a 571 bit binary field
  sect571r1 : NIST/SECG curve over a 571 bit binary field
  c2pnb163v1: X9.62 curve over a 163 bit binary field
  c2pnb163v2: X9.62 curve over a 163 bit binary field
  c2pnb163v3: X9.62 curve over a 163 bit binary field
  c2pnb176v1: X9.62 curve over a 176 bit binary field
  c2tnb191v1: X9.62 curve over a 191 bit binary field
  c2tnb191v2: X9.62 curve over a 191 bit binary field
  c2tnb191v3: X9.62 curve over a 191 bit binary field
  c2pnb208w1: X9.62 curve over a 208 bit binary field
  c2tnb239v1: X9.62 curve over a 239 bit binary field
  c2tnb239v2: X9.62 curve over a 239 bit binary field
  c2tnb239v3: X9.62 curve over a 239 bit binary field
  c2pnb272w1: X9.62 curve over a 272 bit binary field
  c2pnb304w1: X9.62 curve over a 304 bit binary field
  c2tnb359v1: X9.62 curve over a 359 bit binary field
  c2pnb368w1: X9.62 curve over a 368 bit binary field
  c2tnb431r1: X9.62 curve over a 431 bit binary field
  wap-wsg-idm-ecid-wtls1: WTLS curve over a 113 bit binary field
  wap-wsg-idm-ecid-wtls3: NIST/SECG/WTLS curve over a 163 bit binary field
  wap-wsg-idm-ecid-wtls4: SECG curve over a 113 bit binary field
  wap-wsg-idm-ecid-wtls5: X9.62 curve over a 163 bit binary field
  wap-wsg-idm-ecid-wtls6: SECG/WTLS curve over a 112 bit prime field
  wap-wsg-idm-ecid-wtls7: SECG/WTLS curve over a 160 bit prime field
  wap-wsg-idm-ecid-wtls8: WTLS curve over a 112 bit prime field
  wap-wsg-idm-ecid-wtls9: WTLS curve over a 160 bit prime field
  wap-wsg-idm-ecid-wtls10: NIST/SECG/WTLS curve over a 233 bit binary field
  wap-wsg-idm-ecid-wtls11: NIST/SECG/WTLS curve over a 233 bit binary field
  wap-wsg-idm-ecid-wtls12: WTLS curve over a 224 bit prime field
  Oakley-EC2N-3: 
    IPSec/IKE/Oakley curve #3 over a 155 bit binary field.
    Not suitable for ECDSA.
    Questionable extension field!
  Oakley-EC2N-4: 
    IPSec/IKE/Oakley curve #4 over a 185 bit binary field.
    Not suitable for ECDSA.
    Questionable extension field!
  brainpoolP160r1: RFC 5639 curve over a 160 bit prime field
  brainpoolP160t1: RFC 5639 curve over a 160 bit prime field
  brainpoolP192r1: RFC 5639 curve over a 192 bit prime field
  brainpoolP192t1: RFC 5639 curve over a 192 bit prime field
  brainpoolP224r1: RFC 5639 curve over a 224 bit prime field
  brainpoolP224t1: RFC 5639 curve over a 224 bit prime field
  brainpoolP256r1: RFC 5639 curve over a 256 bit prime field
  brainpoolP256t1: RFC 5639 curve over a 256 bit prime field
  brainpoolP320r1: RFC 5639 curve over a 320 bit prime field
  brainpoolP320t1: RFC 5639 curve over a 320 bit prime field
  brainpoolP384r1: RFC 5639 curve over a 384 bit prime field
  brainpoolP384t1: RFC 5639 curve over a 384 bit prime field
  brainpoolP512r1: RFC 5639 curve over a 512 bit prime field
  brainpoolP512t1: RFC 5639 curve over a 512 bit prime field
  SM2       : SM2 curve over a 256 bit prime field

这个列表很长,主要有几个参数:

  1. 什么域:素数域还是 \(GF(2^m)\)
  2. 位数:域有多少位
  3. 标准:NIST/SECG/WTLS/X9.62/RFC 5639/SM2/Oakley 表示的是不同的标准

NIST

NIST 在 FIPS 186-4 中定义了基于素数域的 Curve P-192, Curve P-224, Curve P-256, Curve P-384 和 Curve P-521。在 RFC5656 中,这几条曲线又名 nistp192 nistp224 nistp256 nistp384 和 nistp521。

Curve P-192:

\[p = 2^{192}-2^{64}-1\]

Curve P-224:

\[p=2^{224}-2^{96}-1\]

Curve P-256:

\[p=2^{256}-2^{224}+2^{192}+2^{96}-1\]

Curve P-384:

\[p=2^{384}-2^{128}-2^{96}+2^{32}-1\]

Curve P-521:

\[p=2^{521}-1\]

另一类是基于 Binary Field(\(GF(2^m)\))的曲线,有 Curve K-163,Curve B-163,Curve K-233,Curve B-233,Curve K-283,Curve B-283,Curve K-409,Curve B-409,Curve K-571,Curve B-571。相应地,RFC 5656 里又名 nistk163,nistk233,nistb233,nistk283,nistk409,nistb409,nistt571(我觉得是 nistb571/nistk571,不知道是不是写错了)

Degree 163 (K-163/B-163) :

\[p(t)=t^{163}+t^7+t^6+t^3+1\]

Degree 233 (K-233/B-233) :

\[p(t)=t^{233}+t^{74}+1\]

Degree 283 (K-283/B-283) :

\[p(t)=t^{283}+t^{12}+t^7+t^5+1\]

Degree 409 (K-409/B-409) :

\[p(t)=t^{409}+t^{87}+1\]

Degree 571 (K-571/B-571) :

\[p(t)=t^{571}+t^{10}+t^5+t^2+1\]

SECG

SECG 在 SEC2 中定义了若干的曲线,其中一部分和上面的 NIST 是同一个曲线。首先是基于素数域的:

NIST SEC OID ANSI
nistp192 secp192r1 1.2.840.10045.3.1.1 prime192v1
secp192k1 1.3.132.0.31
nistp224 secp224r1 1.3.132.0.33
secp224k1 1.3.132.0.32
nistp256 secp256r1 1.2.840.10045.3.1.7 prime256v1
secp256k1 1.3.132.0.10
nistp384 secp384r1 1.3.132.0.34
secp384k1
nistp521 secp521r1 1.3.132.0.35
secp521k1

然后是基于 \(GF(2^m)\) 域的:

NIST SEC OID
nistk163 sect163k1 1.3.132.0.1
sect163r1 1.3.132.0.2
nistb163 sect163r2 1.3.132.0.15
nistk233 sect233k1 1.3.132.0.26
nistb233 sect233r1 1.3.132.0.27
sect239k1 1.3.132.0.3
nistk283 sect283k1 1.3.132.0.16
nistb283 sect283r1 1.3.132.0.17
nistk409 sect409k1 1.3.132.0.36
nistb409 sect409r1 1.3.132.0.37
nistk571 (RFC 5656 写的是 nistt571) sect571k1 1.3.132.0.38
nistb571 sect571r1 1.3.132.0.39

sec 命名里,第四个字符里 \(p\) 表示是素数域,\(t\) 表示是 \(GF(2^m)\) 域。后面的字母表示的 \(k\) 表示 Koblitz,\(r\) 表示 random,是参数的选取方式。

OID 有两种前缀:

1.3.132.0.x:
iso(1) identified-organization(3) certicom(132) curve(0)
1.2.840.10045.3.1.x:
iso(1) member-body(2) us(840) 10045 curves(3) prime(1)

完整列表见 OID 1.3.132.0OID 1.2.840.10045.3.1

ANSI

ANSI 也有 X9.62 标准,在附录里面也定义了若干个曲线。附录 J.5.1 里面有三个例子,就是 prime192v1 prime192v2 和 prime192v3,之后则是 prime239v1 prime239v2 prime239v3 和 prime256v1。

ANSI 别名 OID
prime192v1 nistp192/secp192r1 1.2.840.10045.3.1.1
prime192v2 1.2.840.10045.3.1.2
prime192v3 1.2.840.10045.3.1.3
prime239v1 1.2.840.10045.3.1.4
prime239v2 1.2.840.10045.3.1.5
prime239v3 1.2.840.10045.3.1.6
prime256v1 nistp256/secp256r1 1.2.840.10045.3.1.7

总结

对于同一个曲线,不同的组织给出了不同的名字,见下表:

OpenSSL NIST SECG ANSI
prime192v1 nistp192 secp192r1 prime192v1
secp224r1 nistp224 secp224r1
prime256v1 nistp256 secp256r1 prime256v1
secp384r1 nistp384 secp384r1
secp521r1 nistp521 secp521r1
sect163k1 nistk163 sect163k1
sect163r2 nistb163 sect163r2
sect233k1 nistk233 sect233k1
sect233r1 nistb233 sect233r1
sect283k1 nistk233 sect283k1
sect283r1 nistb283 sect283r1
sect409k1 nistk409 sect409k1
sect409r1 nistb409 sect409r1
sect571k1 nistk571 sect571k1
sect571r1 nistb571 sect571r1

在 RFC4492 里也可以看到一个类似的表。

FIDO U2F、FIDO2 和 CTAP 的关系

背景

2012 年,Yubico 和 Google 设计了 U2F 协议,第二年 U2F 成为 FIDO 组织的标准,之后加入了 NFC 的支持。之后,FIDO2 作为替代 U2F 的新标准产生,原来的 U2F 以兼容的方式成为了 CTAP1,而采用 CBOR 封装格式的 CTAP(CTAP2) 则是 FIDO2 的主要协议。

U2F

命令格式

U2F 定义了它的命令格式,基于 ISO7816-4 APDU(short APDU) :

CLA INS P1 P2 Lc data Le
1 byte 1 byte 1 byte 1 byte 0-1 bytes variable length 0-1 bytes

比如 U2F_VERSION 就是:

CLA INS P1 P2 Lc data Le
00 03 00 00 0 empty 00

返回的数据就是 U2F_V2 的 ASCII 加上 9000 的状态。

除此之外,它还有一种 extended length 格式的 APDU,和上面的是等价的不同表示。

传输方式

实际使用 U2F 的时候,又有三种情况,分别是 USB、Bluetooth 和 NFC。

USB

U2FHID 里面,为了让 U2F 的命令通过 HID 接口传输,它规定了两个 endpoint,分别是 Interrupt IN 和 Interrupt OUT,还有一个固定的 HID Report Descriptor。为了发 U2F 命令,首先会进行一次封装:

CMD BCNT DATA
U2FHID_MSG 4..n n bytes

添加了一个头,表示载荷是一个 U2F 的 command(自然也是 APDU)。

在 cmd 之上,还会封装一层,为了解决 USB 的 packet size 限制等问题,定义了 init packet:

CID CMD BCNTH BCNTL DATA
4 bytes 1 byte 1 byte 1 byte variable length

如果数据太长,就会拆分成一个 init 和 多个 continuation packet:

CID SEQ DATA
4 bytes 1 byte variable length

把 init 和 continuation 里面的 data 组合起来,就是 U2F 的 message,message 里面可能又有 U2F raw command,也就是 APDU。

发送的时候,先 Interrupt OUT 发送请求,再 Interrupt IN 读取回应。

Bluetooth

U2F/Bluetooth 里面,也用了一个类似的封装格式,请求:

CMD HLEN LLEN DATA
1 byte 1 byte 1 byte variable length

这里的 DATA payload 就是 extended length 格式的 APDU

NFC

U2F/NFC 里面,既然 ISO 7816-4 本来就是 NFC-native 的格式,就不要额外的封装了。只需要规定一个 Applet 的 AID 即可:A0000006472F0001

总结

总而言之,U2F raw commands 就是在 APDU 格式上定义了几个命令。在 USB 和 Bluetooth 上都加了几个小的 Header,而 NFC 上则是规定了一个 AID。这对应用程序来说很方便,核心的命令只有一套,需要的时候封装一下即可。

FIDO2

在之后,FIDO2 出现了,在保持 U2F 兼容的基础上添加了新的功能,并且出现了 WebAuthN 作为浏览器使用 FIDO2 的协议。U2F 就变成了第一代的 CTAP,称为 CTAP1,然后 CTAP 默认指的就是 CTAP2。

命令格式

FIDO2 里面,定义了一些 CTAP 命令,比如 authenticatorMakeCredential,对应 U2F 的 U2F_REGISTER 命令。然后,规定了一个 CBOR 的格式,来表示命令附带的数据。CBOR 是 RFC 7049,所以也是借用过来的格式。

传输方式

FIDO2 定义了在 USB 和 NFC 上的传输格式。

USB

在 USB 上传输的时候,定义了 CTAPHID 的协议,与 U2FHID 基本是一样的,规定了 init packet 和 continuation packet,packet 里面也是 CTAPHID 的消息,这部分是兼容 U2F 的。并且,额外添加了 CTAPHID_CBOR 消息:

CMD BCNT DATA DATA + 1
CTAPHID_CBOR 1..(n+1) CTAP command n bytes of CBOR data

它的载荷就是 CBOR 格式的请求。

类似地,它也是通过 Interrupt OUT 发送请求,从 Interrupt IN 读取回应。

NFC

在 NFC 上传输的时候,因为内部的格式是 CBOR,不再是 APDU 了,所以需要一些封装。

首先,它也定义了一个 Applet ID:A0000006472F0001,和 U2F 一样。为了保持兼容,它都支持 U2F 定义的 APDU 命令。

那怎么区分设备是否支持 CTAP1/U2F 和 CTAP2 呢?使用前面提到的 U2F_VERSION 命令即可。如果得到 U2F_V2,说明是支持 CTAP1/U2F 的;如果得到是其他的,说明只支持 CTAP2,不支持 CTAP1/U2F。

如果要发 CTAP2 的命令,就要把 CTAP command 和 CBOR 格式的数据封装到 APDU 里面:

CLA INS P1 P2 Data Le
80 10 00 00 CTAP Command || CBOR Data variable

它规定,如果请求采用的是 extended length 的 APDU,那么响应也要是 extended length 的 APDU;如果请求是 short APDU,那么响应也要支持 short APDU 的 chaining。

兼容性

可以看到,CTAP2 设计的基本都考虑了兼容 U2F,允许用 U2F 的 API 操作 U2F 和 CTAP2 的设备;也允许用 CTAP2 的 API 操作 U2F(只支持部分命令)和 CTAP2 的设备。

总结

可以看到,这里有一堆套娃的过程:

U2F:

USB HID Bluetooth NFC
4 APDU APDU APDU
3 U2F message
2 USB HID packet
1 USB Bluetooth ISO 14443-4/ISO 18092

FIDO2:

USB HID NFC
4 CTAP command + CBOR data CTAP command + CBOR data
3 CTAP message APDU
2 USB HID packet
1 USB ISO 14443-4/ISO 18092

USB/IP 模拟 USB 设备

背景

2018 年的时候发过一篇博客,讲如何用 USB/IP 协议在两台 Linux 电脑之间共享 USB 设备。最近刚好有一个需求,就是针对一个现成的 USB device 的代码,通过 USB/IP 模拟出一个 USB 设备,然后进行调试。

USB/IP 协议

USB/IP 只有一个简略的文档,为数不多的使用 USB/IP 的代码,所以有一些细节没有说的很清楚,只能一点点去尝试。

首先,USB/IP 基于 TCP,端口号 3240。客户端向服务端发送请求,服务端向客户端进行回应。

请求的类型:OP_REQ_DEVLIST OP_REQ_IMPORT USBIP_CMD_SUBMIT 和 USBIP_CMD_UNLINK

回应的类型:OP_REP_DEVLIST OP_REP_IMPORT USBIP_RET_SUBMIT USBIP_RET_UNLINK

工作的过程大概如下:

  1. OP_REQ_DEVLIST 请求获取设备列表
  2. OP_REP_DEVLIST 返回设备列表
  3. OP_REQ_IMPORT 请求 USB 设备
  4. OP_REP_IMPORT 返回 USB 设备
  5. USBIP_CMD_SUBMIT 发送 URB
  6. USBIP_RET_SUBMIT 返回 URB

(先不考虑 CMD_UNLINK 和 RET_UNLINK)

其中前面四个比较简单清晰,所需要的字段也都是 Descriptor 中对应的字段。后面两个就相对复杂一些:URB data 的长度需要根据 endpoint 类型和 direction 共同决定。URB 实际上是 Linux 内核里的一个数据结构。

USB 协议

那么,USB 协议的几种 transfer 怎么对应到 URB 的数据呢?首先看最常见的三种([ref](https://www.beyondlogic.org/usbnutshell/usb4.shtml):

  1. Control Transfer
  2. 第一种是 Control IN,一共有三个阶段,第一个阶段是 Setup,Host 发送给 Device 一个八字节的 Setup Packet;第二个阶段是 Data,Device 发送给 Host 一段数据;第三个阶段是 Status,Host 发送给 Device 一个 Zero Length Packet。此时 Setup Packet 对应 urb 中的 setup,Data 就对应 RET_SUBMIT 里面的 URB data 了,自然 CMD_SUBMIT 中是没有 URB data 的
  3. 第二种是 Control OUT,一共有三个阶段,第一个阶段是 Setup,Host 发送给 Device 一个吧字节的 Setup Packet;第二个阶段是 Data,Host 给 Device 发送一段数据;第三个阶段是 Status,Device 给 Host 发送一个 Zero Length Packet。此时 Setup Packet 对应 urb 中的 setup,Data 对应 CMD_SUBMIT 末尾的 URB data,长度由 transfer_buffer_length 指定。返回的 RET_SUBMIT 不带 URB data,但依然需要有 RET_SUBMIT。
  4. Interrupt/Bulk Transfer
  5. 第一种是 Interrupt/Bulk IN,由 Device 给 Host 发送一段数据,附在 RET_SUBMIT 中。
  6. 第二种是 Interrupt/Bulk OUT,由 Host 给 Device 发送一段数据,中 CMD_SUBMIT 的 URB data 中。返回的 RET_SUBMIT 不带 URB data,但不能不发 RET_SUBMIT。

可见,Interrupt 和 Bulk 是比较简单的,而 Control 和 Isochronous(没有提到)则比较复杂。

回到 USB/IP 协议

其实补充了这些信息以后,就可以实现一个 USB/IP 协议的服务器了。

MIFARE Classic 上配置 NDEF

背景

最近买了一堆 NFC 的智能卡拿来测试,其中一张 MIFARE Classic 的总是在 iOS 上读不出来,无论是以 Tag 模式还是 NDEF 模式。于是通过一系列的研究,终于知道上怎么一回事,然后成功地把一个 MIFARE Classic 卡配置成了 NDEF。

背景知识

NFC 有很多协议,其中 MIFARE Classic 基于 ISO 14443-3 Type A 标准,里面有一些 MIFARE 的命令。通过这些命令,就可以控制 MIFARE Classic 卡的内容。具体来说,以我使用的 MIFARE Classic EV1 4K S70 为例,这篇文章会涉及到如下的背景知识:

MIFARE Classic 内存布局

在 MIFARE Classic 中,有 Sector 和 Block 的概念,每个 Sector 有若干个 Block,其中最后一个 Block 是特殊的(称为 Sector Trailer),保存了这个 Sector 的一些信息:Key A、Access Bits、GPB 和 Key B。对于 Classic 4K,首先是 32 个有 4 blocks 的 sector,然后是 8 个 有 16 blocks 的 sector,整体的内存布局大概是:

Sector 0:
    Block 0
    Block 1
    Block 2
    Block 3(Sector Trailer)
Sector 1:
    Block 4
    Block 5
    Block 6
    Block 7(Sector Trailer)
...
Sector 32:
    Block 128
    Block 129
    ...
    Block 143(Sector Trailer)
...
Sector 39:
    ...

每个 Block 有 16 字节,一共 256 个 block,所以是 4K 大小的存储空间。Block 0 比较特殊,保存的是生产商写入的信息,不可更改。

Sector Trailer 的布局如下:

Key A Access Bits GPB Key B
6 字节 3 字节 1 字节 6 字节

其中 Key A 和 Key B 上用于当前 Sector 认证的两个 Key,用相应的 Key 认证以后就可以修改 Sector 里面 Block 的内容。既然有 Key,就会有细粒度的权限控制,就是 Access Bits。它的计算方式比较复杂,首先举个文档AN1305出现过的例子 0x7F 0x07 0x88

  1. 按字节翻转:0x88 0x07 0x7F
  2. 改写成二进制:1000 1000 0000 0111 0111 1111
  3. 拆成前半部分:1000 1000 0000 和后半部分:0111 0111 1111
  4. 如果前后部分互补,说明这是个合法的 Access Bits(这种取反拼接做校验的方法挺常见的)
  5. 取出前半部分:1000 1000 0000
  6. 从后往前取三个字节的最高位:011
  7. 从后往前取第三个字节的次高位,依此类推:000 000 000

这里的 011 表示的是 Sector Trailer 的访问权限,特别地,它表示,不能读出 Key A,只能用 Key B 认证后修改 Key A;用 Key A 或者 Key B 认证后都可以读 Access Bits,但只能在 Key B 认证后修改 Access Bits;不能读出 Key B,只能用 Key B 认证后修改 Key B。也就是说,Key A 认证只能读 Access Bits,而 Key B 认证有权限写入 Key A、Access Bits 和 Key B 字段。完整表格见AN1305 Table 7

之后的三个 000 分别对应前三个 Blocks(又称 Data Blocks,先只考虑带 4 Blocks 的 Sector)的访问权限。000 表示的是,用 Key A 和 Key B 都有完整的读写权限。完整的表格见 AN 1305 Table 8

这里可以给读者留一个练习:0x78 0x77 0x88 对应的权限上什么?

答案:对 Sector Trailer:011;对 Data Blocks:100;此时 Data Blocks 可以用 Key A 或者 Key B 认证读取,但只能用 Key B 认证写入。

如果查看完整的表格就可以发现,Key B 的权限一般是比 Key A 大的,所以 Key B 一般是保密的,而 Key A 可以是公开的。

MIFARE 命令

为了向 MIFARE Classic 卡发送命令,首先需要一个 ISO 14443-3 Type A 的接口,Android 的 NfcA 或者 libnfc 都提供了接口。这里发送的命令实际上会再经过一层解析、用 CRYPTO1 算法加密(猜测是读卡器做的?不是很确定),不过对应用程序来说是透明的。可以参考 MIFARE Classic EV1 1KA Practical Attack on the MIFARE Classic 中的描述。

MIFARE Read

读出一个 Block 的内容,每个 Block 有 16 字节。命令格式如下:

30 XX

如果要读第一个 Block,就是 30 00,如果要读第二个 Block,就是 30 01

返回的数据里刚好是 16 个字节。

MIFARE Write

向一个 Block 写入数据,命令格式如下:

A0 XX YY YY YY YY YY YY YY YY YY YY YY YY YY YY YY YY

这里的 XX 和上面一样,也是 Block 地址;之后是十六字节的数据。

MIFARE Authentiate with A/B

注:这里和 S70 datasheet 里写的不完全一样。

这个命令会进行 Key A 或者 Key B 的认证,如果是对 Key A 认证:

60 XX YY YY YY YY ZZ ZZ ZZ ZZ ZZ ZZ

这里的 XX 也是 Block 地址,但实际上认证的粒度上 Sector,所以只要认证了 Sector 里面的一个 Block,其他 Block 也是同时认证,也是用同一个 Sector Trailer 中的信息进行认证。YY 则是 ISO 14443-3 Type A 中的 UID,如果用 Android 的 API 读取,就可以在 NfcA 中找到这个四字节的信息。ZZ 就是要认证的密钥,六个字节。

如果是对 Key B 认证,把第一个字节的 0x60 改成 0x61 即可。

认证成功后,返回一个 0x00;如果认证失败,则会断开 NFC。

NDEF 是什么

NDEF 实际上是比较高层次的数据,就像 HTML,表示了一个格式化的数组数据,数组的元素可能是文本、URI 等等。它是由若干个 Record 组成的。一个 Record 如下:

03 0B 01 07 54 02 65 6E 61 62 63 64

首先是一个 03 表示类型,然后是长度 0x0B(11,从下一个字节开始数),接着是 0x01 0x07 表示这似乎一个 Well Known 类型的 Record,内容的长度为 7,0x54(ASCII T)表示这是文本格式,0x02 表示编码是 UTF-8,0x65 0x6E (ASCII "en") 表示语言是英语,之后的 0x61 0x62 0x63 0x64(ASCII "abcd")就是文本内容。

很多个 record 连起来,最终一个 0xFE 表示结束,这就是完整的 NDEF 信息了。

在 MIFARE Classic 上使用 NDEF

NDEF 只定义了数据格式,但为了实际使用,还得看具体情况。就好像文件内容保存在硬盘上的时候,并不是直接保存,而是通过文件系统,人为定义一个路径,这样大家才知道要从 /etc/shadow 文件去读 Linux 的用户密码信息,NDEF 也需要人为定义一些规则,再作为数据存放在智能卡里的某个地方,这样大家去读取 metadata,发现上 NDEF Tag,然后才会去解析 NDEF 信息。

有些时候,这个定义很简单,比如直接把 NDEF 数据放在某个 block 里面;有的时候又很复杂,因为可能同时存在很多应用,NDEF 只是其中的一种,所以要有一种类似目录的东西去索引 NDEF“文件”。

MIFARE Classic 上采用的方法上,在特定的 Sector(比如 Sector 0)放一些元数据,元数据里注明了其他的 Sector(从 1 开始的其它 sector)分别用于什么用途,然后 NDEF 是其中一种用途。这个结构叫做 MIFARE Application Directory。具体来说,在 MIFARE Classic 里面,它规定 Block 1 和 Block 2 的内容如下:

0-1 2-3 4-5 6-7 8-9 10-11 12-13 14-15
Info & CRC AID AID AID AID AID AID AID
AID AID AID AID AID AID AID AID

第一个字节是 CRC 8,它的定义可以在这里的 CRC-8/MIFARE-MAD 里找到:初始值 0xC7,多项式上 0x1D。参与 CRC 计算的是按顺序从第二个字节开始的 31 个字节。

第二个字节是 Info Byte,用处不大,见 MAD 的文档。

之后每两个字节对应一个 Sector 的 AID(Application ID),比如 Block 1 的 2-3 字节对应 Sector 1 的 AID,Block 1 的 4-5 字节对应 Sector 2 的 AID,最后 Block 2 的 14-15 字节对应 Sector 15 的 AID。NDEF 的 AID 就是 0x03 0xE1。当软件发现这里的 AID 是 0x03E1 的时候,它就会去相应的 Sector 去读取 NDEF 信息。

一个用 TagInfo 读出来的例子如下:

Sector 0 (0x00)
[skipped]
[01]  F3 01 03 E1 03 E1 00 00
 rW-  00 00 00 00 00 00 00 00
[02]  00 00 00 00 00 00 00 00
 rW-  00 00 00 00 00 00 00 00
[03]  A0:A1:A2:A3:A4:A5  MAD access key
 WXW  78:77:88 C1
      XX:XX:XX:XX:XX:XX  (key unavailable)

可以看到,这里表示的是 Sector 1 和 Sector 2 是 03E1 NDEF。下面 [03] 行表示的是 Key A,下一行是 Access bits、GPD,最后一行是 Key B。TagInfo 会尝试从 well known 里的 Key A 和 Key B 一个个试,直到认证成功为止。常见的如下:

  1. A0 A1 A2 A3 A4 A5:MAD 的 Key A
  2. D3 F7 D3 F7 D3 F7:NDEF 的 Key A
  3. FF FF FF FF FF FF:出场默认的 Key A 和 Key B

如何在 MIFARE Classic 上配置 NDEF

如果看了这么多背景知识,你还有心情看到这里,那要给个掌声。

为什么要在 MIFARE Classic 上配置 NDEF 呢?因为直接买到的 MIFARE Classic(比如我用的 EV1 4K S70)里面都是出厂状态,Key A 和 Key B 都是 FF FF FF FF FF FF,除了 Block 0 以外数据都是 0,所以它并不能用作 NDEF,Android 也只是认为它 NdefFormattable。所以我们要做的就是,Format as NDEF。为啥要自己搞呢,也是因为试了几个现成的工具 format 都失败了。

其实整个流程在 AN1305 的 8.1 章节都写了,但看起来简单,实现起来还是有很多细节,在搞的时候也是来来回回做了很多尝试,同时也利用 TagInfo 强大的 Memory dump 配合调试。

首先复习一下我们可以用哪些命令:

  1. MIFARE Authenticate:对一个 sector 认证,认证成功了才能写操作
  2. MIFARE Read:读取一个 Block
  3. MIFARE Write:写入一个 Block

仔细观察 AN1305 的 Fig.10 和下面的文本描述,大概需要做这些事情:

  1. 修改 Block 1 和 Block 2 中的信息,符合 MAD 的格式
  2. 修改 Sector 0 的 Sector Trailer
  3. 修改 Block 4,填入一个空白的 NDEF,或者直接前面背景知识里的例子。
  4. 修改 Sector 1 和 Sector 2 的 Sector Trailer

但有一些细节:

  1. 修改 Sector Trailer 的时候要谨慎,因为会修改 Key,如果改完又忘了,这卡就废了
  2. 注意用 Key A 还是 Key B 进行认证。上面这些流程结束后,Sector 0 被保护了,需要用 Key B 才能修改数据;而 Sector 1 和 Sector 2 是开放的;如果执行完第一步和第二步以后,发现第一步写错了,就要注意权限的问题,必要时还可以先修改 Access bits 再修改数据
  3. 在这里为了简单,Key B 都用 FF FF FF FF FF FF 了,实际情况下可以用别的自己的密钥,只要记住就行

那么,按照前面的这些知识,就可以构造出每一步的 MIFARE 命令了:

注意:下面的命令不一定能工作,在执行前请仔细理解每条命令的结果,本文作者对卡的损失概不负责

第一步:

60 00 YY YY YY YY FF FF FF FF FF FF
A0 01 F3 01 03 E1 03 E1 00 00 00 00 00 00 00 00 00 00
A0 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

注意 YY 要填入 ID。这一步首先用 FF FF FF FF FF FF 认证了 Sector 0 的 Key A,然后写入了 Block 1 和 Block 2。Info Byte 用的是 0x01,然后用在线工具计算了一下 CRC=F3。

第二步:

A0 03 A0 A1 A2 A3 A4 A5 78 77 88 C1 FF FF FF FF FF FF

这一步设置了 Key A 为 MAD access key,权限是 78 77 88,GPB 是 C1,Key B 为 FF FF FF FF FF FF

第三步:

60 04 YY YY YY YY FF FF FF FF FF FF
A0 04 00 00 03 0B D1 01 07 54 02 65 6E 61 62 63 64 FE

这一步认证了 Sector 1,然后往 Block 4 写入了一个 abcd 的 NDEF 记录。

第四步:

A0 07 D3 F7 D3 F7 D3 F7 7F 07 88 40 FF FF FF FF FF FF
60 08 YY YY YY YY FF FF FF FF FF FF
A0 0B D3 F7 D3 F7 D3 F7 7F 07 88 40 FF FF FF FF FF FF

写入了 Sector 1 的 Sector Trailer,然后认证 Sector 2,再写入 Sector 2 的 Sector Trailer

这样就完成了,再用 TagInfo 等软件,就可以读取出来 NDEF 信息了。此时 iOS 也可以读出来。

上面这些过程,在实际情况下在不同 sector 的时候需要打断,每次重新认证一下。这里默认了一些卡的初始密钥,如果初始情况并不一致,可能并不会工作。

踩的坑

在这个过程中踩过很多的坑:

  1. 在空 NDEF 的时候,NFC Tools 能读出来是 Ndef,并且内容是空,但写入的时候表示 Write error,也读不出来;去 TagInfo 读内存,发现确实写进去了,但内容不对,有一个位置的长度写成了 0,可能是 BUG
  2. 上面也提到过的,就是在修改为只读以后,发现数据写错了,只好重新改成可写,把数据改好了以后再设为只读。
  3. iOS 上用 NFCNDEFReaderSession 可以读出来这个 NDEF 的内容,但 NFCTagReaderSession 并不能 poll 出来。

解释 BB84 协议

背景

这周密码学课程,来自 BUPT 的高飞老师讲了一下 qkd 里的 BB84 协议,老师讲得很好,我也想记录一下这个协议的流程和方法。我不是这方面的专业人士,如果有什么问题请指出。

背景知识

QKD(Quantum Key Distribution)目的是让通信双方获得同一个密钥,它需要同时需要量子信道和经典信道。其中经典信道被认为是可信的,它可以被监听,但不能被中间人攻击。

在 BB84(Charles H. Bennett and Gilles Brassard (1984))协议中,传输的是一个光子,它具有如下的特性:

可以用两个基去测量光子:➕️和✖️️,然后光子有四个偏振角度,分别是 ⬆️️ ⬇️️ ↘️️ ↗️️。定义一个二进制位和偏振角度的对应关系如下:

0 1 0 1
➕️ ➕️ ✖️️ ✖️️
偏振角度 ⬆️️ ⬇️️ ↗️️ ↘️️

对于一个未知光子,可以用两种基进行测量,测量的结果:

偏振角度 ⬆️️ ⬇️️ ↗️️ ↘️️
用➕️测量 0 1 0/1 0/1
用✖️️测量 0/1 0/1 0 1

这里的 0/1 表示有 50% 概率测得 0,有 50% 概率测得 1。

协议流程

假如 Alice 要和 Bob 进行 BB84 协议。那么,Alice 首先随机生成一段二进制序列,并随机生成一个基的序列,以 Wikipedia 上的例子为例:

Alice's random bit 0 1 1 0 1 0 0 1
Alice's random sending basis ➕️ ➕️ ✖️️ ➕️ ✖️️ ✖️️ ✖️️ ➕️
Photon polarization Alice sends ⬆️️ ➡️️ ↘️️ ⬆️️ ↘️️ ↗️️ ↗️️ ➡️️
Bob 生成随机的基 ➕️ ✖️️ ✖️️ ✖️️ ➕️ ✖️️ ➕️ ➕️
Photon polarization Bob measures ⬆️️ ↗️️ ↘️️ ↗️️ ➡️️ ↗️️ ➡️️ ➡️️
Bob 认为的二进制信息 0 0 1 0 1 0 1 1
通过经典信道交换信息
Shared secret key 0 丢弃 1 丢弃 丢弃 0 丢弃 1

第一步,Alice 生成随机的二进制和随机的基,按照前面谈到过的对应关系,生成带有偏振角度的光子发给 Bob。

第二步,由于 Bob 只收到光子,不知道 Alice 选取的基底信息,而且只能用一个基测量一次,所以 Bob 随机从两种基选择一个来测量,得到了一串二进制。这些二进制里,如果 Alice 和 Bob 选取了同一个基,那么这一位的数据一定是对的;如果选取了不同的基,那么这一位有一半的可能是对的。总的来说,期望有四分之一的位是不正确的。

第三步,Alice 和 Bob 在 可信 的经典信道中把双方的基底进行对比,把基底相等的部分对应的二进制位提取出来,作为最终使用的密钥。

第四步,Alice 和 Bob 在最终使用的密钥中抽取若干位,然后对比,如果这些位都一致,则这个密码是有效的。如果错误率太高,那么很大概率是被攻击了。

安全性

协议的安全性,主要是靠量子的特性:未知量子态不可克隆、对未知量子态的测量可能会改变量子态。

假如在量子信道中间有一个 Eve 想要做坏事,它如果在中间观测了一下光子,它就会影响光子的量子态,导致 Bob 的密钥和 Alice 密钥会不一致,从而在协议的第四步被发现。并且,因为 Eve 并不知道 Alice 所使用的基底(假设 Eve 只能控制量子信道、不能控制经典信道),所以得到的二进制数据也有四分之一是不正确的。即使 Eve 尝试截获并且重发光子给 Bob,Bob 得到的密钥仍然有很高的错误率。通过这个错误率,就可以判断是否被攻击了。

另外,它的安全性还依赖以下几个方面:

  1. Eve 不能控制 Alice 和 Bob 的量子密码设备
  2. Alice 和 Bob 的随机数生成器需要足够安全
  3. 经典信道是可信的

引用

Quantum key distribution - Wikipedia

在 k8s 中部署 code-server

实验了一下在 k8s 中部署 code-server,并不复杂,和之前几篇博客的配置是类似的:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: code
  labels:
    app: code
spec:
  selector:
    matchLabels:
      app: code
  replicas: 1
  template:
    metadata:
      labels:
        app: code
    spec:
      volumes:
        - name: code-volume
          persistentVolumeClaim:
              claimName: code-pvc
      initContainers:
      - name: home-init
        image: busybox
        command: ["sh", "-c", "chown -R 1000:1000 /home/coder"]
        volumeMounts:
        - mountPath: "/home/coder"
          name: code-volume
      containers:
      - image: codercom/code-server:latest
        imagePullPolicy: Always
        name: code
        volumeMounts:
          - mountPath: "/home/coder"
            name: code-volume
        resources:
          limits:
            cpu: "0.5"
            memory: "500Mi"
        ports:
        - containerPort: 8080
        env:
          - name: PASSWORD
            value: REDACTED

---
apiVersion: v1
kind: Service
metadata:
  name: code
  labels:
    app: code
spec:
  ports:
    - port: 8080
  selector:
    app: code

---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: ingress-code
  annotations:
    kubernetes.io/ingress.class: "nginx"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.org/websocket-services: "code"
spec:
  tls:
  - hosts:
    - example.com
    secretName: code-tls
  rules:
  - host: example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: code
          servicePort: 8080

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: code-pvc
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 1Gi

需要注意的几个点:

  1. 用了一个 pvc 用于 /home/coder 的持久化,所以你的集群里得有相应的 pv/storage class
  2. 我用的是 Nginx Inc. 的 ingress controller,它的 websocket 支持需要一句 nginx.org/websocket-services 设置
  3. 额外添加了一个 init container,为了处理 home 目录的权限

在 k8s 中部署 Drone 用于 CI

实验了一下在 k8s 中部署 CI,在 drone gitlab-ci 和 jenkins 三者中选择了 drone,因为它比较轻量,并且基于 docker,可以用 GitHub 上的仓库,比较方便。

首先,配置 helm:

helm repo add drone https://charts.drone.io
kubectl create ns drone

参考 drone 的文档,编写 drone-values.yml:

ingress:
  enabled: true
  annotations:
    kubernetes.io/ingress.class: "nginx"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
  hosts:
    - host: drone.example.com
      paths:
        - "/"
  tls:
  - hosts:
    - drone.example.com
    secretName: drone-tls
env:
  DRONE_SERVER_HOST: drone.example.com
  DRONE_SERVER_PROTO: https
  DRONE_USER_CREATE: username:YOUR_GITHUB_USERNAME,admin:true
  DRONE_USER_FILTER: YOUR_GITHUB_USERNAME
  DRONE_RPC_SECRET: REDACTED
  DRONE_GITHUB_CLIENT_ID: REDACTED
  DRONE_GITHUB_CLIENT_SECRET: REDACTED

需要首先去 GitHub 上配置 OAuth application,具体参考 drone 的文档。然后,生成一个 secret,设置 admin 用户并只允许 admin 用户使用 drone,防止其他人使用。然后应用:

helm install --namespace drone drone drone/drone -f drone-values.yml
# or, to upgrade
helm upgrade --namespace drone drone drone/drone --values drone-values.yml 

然后就可以访问上面配好的域名了。遇到了 cert manager 最近的一个 bug,来回折腾几次就好了。

接着配 drone 的 k8s runnner,也是参考 drone 的文档,编写 drone-runner-kube-values.yml:

rbac:
  buildNamespaces:
    - drone
env:
  DRONE_RPC_SECRET: REDACTED
  DRONE_NAMESPACE_DEFAULT: drone

然后应用:

helm install --namespace drone drone-runner-kube drone/drone-runner-kube -f drone-runner-kube-values.yml

然后就可以去 drone 界面上操作了。

需要注意的是,drone 需要 pv,所以我先在腾讯云里面配置了 CFS 的 storage class,然后它就会自动 provision 一个新的 pv 和 pvc 出来。

接着尝试了一下在 drone 里面构建 docker 镜像并且 push 到 registry 上。以腾讯云为例:

kind: pipeline
type: kubernetes
name: default

steps:
- name: build
  image: alpine
  commands:
  - make

- name: publish
  image: plugins/docker
  settings:
    registry: ccr.ccs.tencentyun.com
    repo: ccr.ccs.tencentyun.com/abc/def
    tags: ["${DRONE_COMMIT_SHA:0:7}","latest"]
    username:
      from_secret: docker_username
    password:
      from_secret: docker_password

然后在网页里配置好 docker username 和 password,它就会自动构建 docker 镜像并且 push 到 registry 上,然后再 rollout 一下 deployment 就能部署最新的 image 了。实际上可以在 drone 里面把部署这一步也完成,但目前还没有去实践。

参考文档:

Drone provider: GitHub

Drone helm chart

Drone runner kube helm chat

Building a CD pipeline with drone CI and kubernetes

在 k8s 内用 Cert Manager 配合 Nginx Ingress Controller 配置 Let's Encrypt HTTPS 证书

上一篇博客讲了 nginx ingress 的配置,那自然第一步要配上 https。首先配置 cert-manager:

$ kubectl create namespace cert-manager
$ kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v0.14.1/cert-manager.crds.yaml
$ helm repo add jetstack https://charts.jetstack.io
$ helm repo update
$ helm install \
  cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --version v0.14.1

然后,配置 Cluster Issuer,应用以下的 yaml:

apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
  namespace: cert-manager
spec:
  acme:
    email: example@example.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - http01:
        ingress:
          class: nginx

然后在 ingress 里面进行配置:

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: ingress-example
  annotations:
    kubernetes.io/ingress.class: "nginx"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  tls:
  - hosts:
    - example.com
    secretName: example-tls
  rules:
  - host: example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: example-service
          servicePort: 80

应用以后,用 kubectl describe certificate 查看证书获取进度。成功后,访问改域名的 HTTP,就会自动跳转到 HTTPS,并且提供了正确的证书。

在 TKE 上配置不使用 LB 的 Nginx Ingress Controller

背景

想要在 k8s 里面 host 一个网站,但又不想额外花钱用 LB,想直接用节点的 IP。

方法

首先安装 nginx-ingress:

$ helm repo add nginx-stable https://helm.nginx.com/stable
$ helm repo update
$ helm install ingress-controller nginx-stable/nginx-ingress --set controller.service.type=NodePort --set controller.hostNetwork=true

这里给 ingress controller chart 传了两个参数:第一个指定 service 类型是 NodePort,替代默认的 LoadBalancer;第二个指定 ingress controller 直接在节点上监听,这样就可以用节点的公网 IP 访问了。

然后配一个 ingress:

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: ingress-example
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
  - host: example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: example-service
          servicePort: 80

然后就可以发现请求被正确路由到 example-service 的 80 端口了。

在 sbt 中 fork 并且并行运行测试

问题

最近在 sbt 使用遇到一个问题,有两个测试,分别用 testOnly 测试的时候没有问题,如果同时测试就会出问题,应该是全局的状态上出现了冲突。一个自然的解决思路是 fork,但是 sbt 默认 fork 之后 test 是顺序执行的,这会很慢。所以搜索了一下,找到了 fork 并且并行测试的方法。

解决方法

解决方法在 sbt 文档中其实就有(原文)。简单来说就是:把每个 test 放到单独的 TestGroup 中,每个 TestGroup 分别用一个 forked JVM 去运行;然后让 sbt 的并行限制设高一些:

// move each test into a group and fork them to avoid race condition
import Tests._
def singleTests(tests: Seq[TestDefinition]) =
  tests map { test =>
    new Group(
      name = test.name,
      tests = Seq(test),
      SubProcess(ForkOptions()))
  }

Test / testGrouping := singleTests( (Test / definedTests).value )
// allow multiple concurrent tests
concurrentRestrictions in Global := Seq(Tags.limitAll(4))

这样就可以了。

在命令行中进行 Vivado 仿真

已有 Vivado 项目

想要在命令行里进行 Vivado 仿真,所以查了下 Xilinx 的 UG900 文档,找到了命令行仿真的方法。首先是生成仿真所需的文件:

# assuming batch mode
open_project xxx.xpr
set_property top YOUR_SIM_TOP [current_fileset -simset]
export_ip_user_files -no_script -force
export_simulation -simulator xsim -force

可以把这些语句放到 tcl 文件里然后用 batch mode 执行。执行成功以后,会在 export_sim/xsim 目录下生成一些文件。里面会有生成的脚本以供仿真:

cd export_sim/xsim && ./YOUR_SIM_TOP.sh

默认情况下它会执行 export_sim/xsim/cmd.tcl 里面的命令。如果想要记录 vcd 文件,修改内容为:

open_vcd
log_vcd
run 20us
close_vcd
quit

这样就可以把仿真的波形输出到 dump.vcd 文件,拖到本地然后用 GTKWave 看。更多支持的命令可以到 UG900 里找。

无项目模式

如果没有创建 Vivado 项目,也可以单独进行仿真,具体分为三个步骤:

  1. 第一步,对每个源 Verilog 文件,运行 xvlog module.v 命令
  2. 第二步,生成 snapshot,运行 xelab -debug all --snapshot snapshot_name top_module_name
  3. 第三步,仿真,运行 xsim snapshot_name

如果想要生成波形文件,编辑 xsim.tcl 为以下内容:

open_vcd
log_vcd *
run -all
close_vcd
quit

把第三步运行的命令改为:xsim snapshot_name -tclbatch xsim.tcl 即可。

体验 Tencent Kubernetes Engine

之前在机器上试验了一下 kubernetes,感觉挺不错的,所以就想把腾讯云上面的机器也交给 kubernetes 管理。找到容器服务,新建集群,选择模板里的标准托管集群就可以了。然后开启下面的公网访问,设置一个比较小的 IP 地址段,按照页面下面的要求合并一下 kube config(因为还有别的 k8s cluster):

$ KUBECONFIG=~/.kube/config:/path/to/new/config kubectl config view --merge --flatten > new_config
$ cp new_config ~/.kube/config

覆盖之前先确认原来的配置还在,然后就可以用 kubectl 切换到新的 context:

$ kubectl config get-contexts
$ kubectl config use-context new-context-here
$ kubectl get node
NAME          STATUS   ROLES    AGE   VERSION
172.21.0.17   Ready    <none>   75m   v1.16.3-tke.3

可以看到我们的 k8s node 已经上线了。我一般习惯先配好 kubernetes-dashboard:

$ kubectl create -f https://raw.githubusercontent.com/cilium/cilium/v1.6/install/kubernetes/quick-install.yaml
$ kubectl proxy &
$ kubectl -n kubernetes-dashboard describe secret (kubectl -n kubernetes-dashboard get secret | grep admin-user | awk '{print \$1}') | tail -n1 | awk '{print \$2}' | pbcopy

然后在浏览器里访问 http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/#/overview?namespace=default 然后把剪贴板里的 token 粘贴进去即可。

默认情况下 kubernetes-dashboard 的权限比较少,可以让它获得更多权限:

$ kubectl edit clusterrole.rbac.authorization.k8s.io/kubernetes-dashboard
# use '*' for ultimate 
# use `kubectl get clusterrole.rbac.authorization.k8s.io/cluster-admin -o yaml` to see full permissions

接下来配置 metrics-server。下载 metrics-server 仓库,然后修改镜像地址:

$ wget https://github.com/kubernetes-sigs/metrics-server/archive/v0.3.6.zip
$ unar v0.3.6.zip
$ cd metrics-server-0.3.6/deploy/1.8+
$ vim metrics-server-deployment
# change: k8s.gcr.io/metrics-server-amd64:v0.3.6
# to: registry.cn-hangzhou.aliyuncs.com/google_containers/metrics-server-amd64:v0.3.6
# add line below image: args: ["--kubelet-insecure-tls"]
$ kubectl apply -f .

等一段时间,就可以看到 metrics server 正常运行了。

参考:https://tencentcloudcontainerteam.github.io/tke-handbook/

在 Rocket Chip 上挂接 TLRAM

最近遇到一个需求,需要在 Rocket Chip 里面开辟一块空间,通过 verilog 的 $readmemh 来进行初始化而不是用 BootROM,这样每次修改内容不需要重新跑一次 Chisel -> Verilog 的流程。然后到处研究了一下,找到了解决的方案:

首先是新建一个 TLRAM 然后挂接到 cbus 上:

import freechips.rocketchip.tilelink.TLRAM
import freechips.rocketchip.tilelink.TLFragmenter
import freechips.rocketchip.diplomacy.LazyModule
import freechips.rocketchip.diplomacy.AddressSet

trait HasTestRAM { this: BaseSubsystem =>
  val testRAM = LazyModule(
    new TLRAM(AddressSet(0x40000000, 0x1FFF), beatBytes = cbus.beatBytes)
  )

  testRAM.node := cbus.coupleTo("bootrom") { TLFragmenter(cbus) := _ }
}

这里的地址和大小都可以自由定义。然后添加到自己的 Top Module 中:

class TestTop(implicit p:Parameters)
    extends RocketSystem
    // ...
    with HasTestRAM
    //...
    {
    override lazy ...    
}

实际上这时候 TLRAM 就已经加入到了 TileLink 总线中。接着,为了让 firrtl 生成 $readmemh 的代码,需要两个步骤:

首先是用 chisel3.util.experimental.loadMemoryFromFile 函数(文档在 https://github.com/freechipsproject/chisel3/wiki/Chisel-Memories):

UPDATE:现在的文档在 Loading Memories for simulation or FPGA initialization 处,并且可以用 loadMemoryFromFileInline。

class TestTopImp(outer: TestTop)
    extends RocketSubsystemModuleImp(outer)
    // ...
    {
    loadMemoryFromFile(outer.testRAM.module.mem, "test.hex")    
}

这个函数会生成一个 FIRRTL Annotation,记录了在这里需要对这个 mem 生成对应的 readmemh 调用。然后在 firrtl 的调用里传入 .anno.json 和 transform:

$ runMain firrtl.stage.Main -i xxx -o xxx -X verilog -faf /path/to/xxx.anno.json -fct chisel3.util.experimental.LoadMemoryTransform

UPDATE: 现在不需要 -fct chisel3.util.experimental.LoadMemoryTransform 参数。目前这个功能和生成 blackbox memory 有冲突,不能同时使用,需要等 chisel3 后续修复。

这里的 chisel3.util.experimental.LoadMemoryTransform 会找到 anno.json 里面对应的 Annotation,然后生成类似下面这样的 verilog 代码:

module xxx(
    // ...
);
  // ...
    $readmemh(path, mem_xxx);
endmodule

bind TLRAM xxx xxx(.*);

这里采用了 Verilog 的 bind 功能,可以在不修改模块代码的时候注入,比如上面,就是注入了一个语句 $readmemh,从而达到目的。

在 Kubernetes 集群上部署 gitlab—runner

按照 GitLab 上的教程试着把 gitlab-runner 部署到 k8s 集群上,发现异常地简单,所以简单做个笔记:

编辑 values.yaml

gitlabUrl: GITLAB_URL
runnerRegistrationToken: "REDACTED"
rbac:
    create: true

此处的信息按照 "Set up a specific Runner manually" 下面的提示填写。然后用 Helm 进行安装:

$ helm repo add gitlab https://charts.gitlab.io
$ kubectl create namespace gitlab-runner
$ helm install --namespace gitlab-runner gitlab-runner -f values.yaml gitlab/gitlab-runner

然后去 Kubernetes Dashboard 就可以看到部署的情况,回到 GitLab 也可以看到出现了“Runners activated for this project” ,表示配置成功。

参考配置:https://docs.gitlab.com/runner/install/kubernetes.html

用 Kubernetes 部署无状态服务

背景

最近需要部署一个用来跑编译的服务,服务从 MQ 取任务,编译完以后提交任务。最初的做法是包装成 docker,然后用 docker-compose 来 scale up。但既然有 k8s 这么好的工具,就试着学习了一下,踩了很多坑,总结了一些需要用到的命令。

搭建 Docker Registry

首先搭建一个本地的 Docker Repository,首先设置密码:

$ mkdir auth
$ htpasswd user pass > auth/passwd

然后运行 registry:

$ docker run -d -p 5000:5000 \
        --restart=always \
        --name registry \
        -v "$(pwd)/registry":/var/lib/registry \
        -v "$(pwd)/auth":/auth \
        -e "REGISTRY_AUTH=htpasswd" \
        -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
        -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
        registry:2

简单起见没有配 tls。然后吧本地的 image push 上去:

$ docker tag $image localhost:5000/$image
$ docker push localhost:5000/$image

这样就可以了。

搭建 k8s 环境

考虑到只用了单个物理机,所以采用的是 minikube。首先下载 minikube,下载方法略去。

接着新建 minikube 虚拟机:

$ minikube start --registry-mirror=https://registry.docker-cn.com --image-mirror-country=cn --image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers --vm-driver=kvm2 --insecure-registry="0.0.0.0/0" --disk-size=50GB --cpus 128 --memory 131072

这里的 0.0.0.0/0 可以缩小,磁盘、CPU 和内存需要在这里就设好,之后不能改,要改只能重新开个虚拟机,不过这个过程也挺快的。

然后初始化一些组件(metrics server 和 kubernetes dashboard):

$ minikube addons enable metrics-server
$ minikube dashboard

如果要访问 dashboard 的话,可以用上面命令输出的链接,或者用 kubectl proxy 然后打开 http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ (注意 http 还是 https)。

如果问到 Access Token,可以用以下 alias 获得(fish/macOS):

$ alias kubedashboard="kubectl -n kubernetes-dashboard describe secret (kubectl -n kubernetes-dashboard get secret | grep admin-user | awk '{print \$1}') | tail -n1 | awk '{print \$2}' | pbcopy"

接着,配置一下 docker registry 的密钥:

$ kubectl create secret generic regcred --from-file=.dockerconfigjson=/path/to/config.json  --type=kubernetes.io/dockerconfigjson

然后,在 Pod/Deployment 里面设定镜像:

containers:
  - name: name
    image: IP:5000/image
imagePullSecrets:
  - name: regcred

然后部署即可。

部署水平自动伸缩(HPA)

这一步配置的是自带的 HPA 功能,需要上述的 metrics-server 打开,并且在 Pod/Deployment 里面写明 resources.requests.cpu:

- name: name
  resources:
    requests:
      cpu: "xxx"

然后创建 HPA 即可:

$ kubectl autoscale deployment $deployment --cpu-percent=50 --min=1 --max=10

通过压测,可以看到自动伸缩的记录:

$ kubectl describe hpa
Normal  SuccessfulRescale  22s   horizontal-pod-autoscaler  New size: 4; reason: cpu resource utilization (percentage of request) above target
Normal  SuccessfulRescale  6s     horizontal-pod-autoscaler  New size: 1; reason: All metrics below target

参考:Kubernetes 官方文档

用 jailkit 限制用户仅 scp

最近需要用 scp 部署到生产机器,但又不想出现安全问题,所以用了 jailkit 的方法。首先是创建单独的用户,然后生成 ssh key 来认证,不再赘述。此时是可以 scp 了,但用户依然可以获得 shell,不够安全。

然后找到了下面参考链接,大概摘录一下所需要的命令和配置:

mkdir /path/to/jail
chown root:root /path/to/jail
chmod 701 /path/to/jail
jk_init -j /path/to/jail scp sftp jk_lsh
jk_jailuser -m -j /path/to/jail jailed_user
vim /path/to/jail/etc/jailkit/jk_lsh.ini
# Add following lines
[jailed_user]
paths = /usr/bin, /usr/lib
exectuables = /usr/bin/scp

之后可以发现该用户的 shell 已经更改 jk_chrootsh,并且只能用 scp。

参考:https://blog.tinned-software.net/restrict-linux-user-to-scp-to-his-home-directory/

每周分享第 56 期

咕咕咕

  1. SystemVerilog linter https://github.com/dalance/svlint
  2. 东北方言编程语言 https://github.com/zhanyong-wan/dongbei
  3. JS LaTeX 渲染到 HTML https://github.com/michael-brade/LaTeX.js
  4. 一种对语音助手的攻击 https://surfingattack.github.io/
  5. 在线打铃网站 http://thulpwan.net/timer/
  6. 网络学堂 PC 端 App https://github.com/jiegec/learn_tsinghua_app/releases
  7. Rust 2020 roadmap https://github.com/rust-lang/rfcs/pull/2857/files

通过 BSCAN JTAG 对 Rocket Chip 进行调试

前言

在上一个 post 里研究了原理,今天也是成功在 Artix 7 上实现了调试。效果如下:

OpenOCD 输出:

Info : JTAG tap: riscv.cpu tap/device found: 0x0362d093 (mfg: 0x049 (Xilinx), part: 0x362d, ver: 0x0)
Info : datacount=1 progbufsize=16
Info : Disabling abstract command reads from CSRs.
Info : Examined RISC-V core; found 1 harts
Info :  hart 0: XLEN=32, misa=0x40801105
Info : Listening on port 3333 for gdb connections

GDB 输出:

Remote debugging using localhost:3333
0x0001018c in getc () at bootloader.c:36
36        while (!(*UART_LSR & 0x1))
(gdb) 

这里用的 OpenOCD 和 GDB 都是 riscv 版本,上游的支持尚不完善。对于 Homebrew 用户,我在 jiegec/homebrew-formulas 维护了需要的 Formula。

过程

代码基本借鉴了 sequencer/rocket-playgroundKireinaHoro/rocket-zcu102 而来,代码方面主要是添加了 BscanJTAG.scala,然后在 Top 模块下把它连接到内部的 JTAG 中:

val boardJTAG = Module(new BscanJTAG)
val jtagBundle = target.debug.head.systemjtag.head

// set JTAG parameters
jtagBundle.reset := reset
jtagBundle.mfr_id := 0x233.U(11.W)
jtagBundle.part_number := 0.U(16.W)
jtagBundle.version := 0.U(4.W)
// connect to BSCAN
jtagBundle.jtag.TCK := boardJTAG.tck
jtagBundle.jtag.TMS := boardJTAG.tms
jtagBundle.jtag.TDI := boardJTAG.tdi
boardJTAG.tdo := jtagBundle.jtag.TDO.data
boardJTAG.tdoEnable := jtagBundle.jtag.TDO.driven

代码方面就足够了。然后,需要一个 riscv-openocd 和 riscv-gdb,分别从上游 repo 编译得来。然后采用以下的 openocd.cfg:

adapter_khz 20000
interface ftdi
ftdi_vid_pid 0x0403 0x6014
ftdi_layout_init 0x00e8 0x60eb
ftdi_tdo_sample_edge falling
reset_config none

set _CHIPNAME riscv
jtag newtap $_CHIPNAME cpu -irlen 6

set _TARGETNAME $_CHIPNAME.cpu

target create $_TARGETNAME.0 riscv -chain-position $_TARGETNAME
$_TARGETNAME.0 configure -work-area-phys 0x80000000 -work-area-size 10000 -work-area-backup 1
riscv use_bscan_tunnel 5

然后就可以用 GDB 调试了。