├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ ├── run-tests.yaml │ └── docker-image.yaml ├── papers ├── fips186-2.pdf ├── validation.pdf ├── drunken_bishop.pdf └── ed25519-20110926.pdf ├── Makefile ├── t └── test-keys │ ├── id_ed25519.pub │ ├── id_ed25519_key_type_mismatch.pub │ ├── id_ed25519_encrypted_3des-cbc.pub │ ├── id_ed25519_encrypted_aes128-ctr.pub │ ├── id_ed25519_encrypted_aes192-cbc.pub │ ├── id_ed25519_encrypted_aes192-ctr.pub │ ├── id_ed25519_encrypted_aes256-cbc.pub │ ├── id_ed25519_encrypted_aes256-ctr.pub │ ├── id_ecdsa_nistp256.pub │ ├── id_ecdsa_nistp384.pub │ ├── id_rsa_1024.pub │ ├── id_ecdsa_nistp521.pub │ ├── id_ed25519 │ ├── id_ed25519_key_type_mismatch │ ├── id_ed25519_encrypted_3des-cbc │ ├── id_ed25519_encrypted_aes128-ctr │ ├── id_ed25519_encrypted_aes192-cbc │ ├── id_ed25519_encrypted_aes192-ctr │ ├── id_ed25519_encrypted_aes256-cbc │ ├── id_ed25519_encrypted_aes256-ctr │ ├── id_ecdsa_nistp256 │ ├── id_rsa_3072.pub │ ├── id_rsa_missing_key_type.pub │ ├── id_rsa_3072_encrypted_aes128-cbc.pub │ ├── id_rsa_3072_host_ca.pub │ ├── id_rsa_3072_user_ca.pub │ ├── id_rsa_unknown_key_type.pub │ ├── id_dsa.pub │ ├── id_ecdsa_nistp384 │ ├── id_ecdsa_nistp521 │ ├── id_rsa_1024 │ ├── id_dsa │ ├── id_ed25519-cert.pub │ ├── id_ecdsa_nistp256-cert.pub │ ├── id_ecdsa_nistp384-cert.pub │ ├── id_ecdsa_nistp521-cert.pub │ ├── id_rsa_3072-cert.pub │ ├── id_dsa-cert.pub │ ├── id_rsa_3072 │ ├── id_rsa_unknown_key_type │ ├── id_rsa_3072_host_ca │ ├── id_rsa_3072_user_ca │ ├── id_rsa_3072_encrypted_aes128-cbc │ └── id_rsa_invalid_padding ├── Dockerfile ├── Dockerfile.ccl ├── Dockerfile.ecl ├── Dockerfile.sbcl ├── scripts ├── run-tests.sh └── run-ci-tests.sh ├── cl-ssh-keys.test.asd ├── LICENSE ├── cl-ssh-keys.asd ├── src ├── generics.lisp ├── base.lisp ├── conditions.lisp ├── signature.lisp ├── key-types.lisp ├── ciphers.lisp ├── rfc8017.lisp ├── package.lisp ├── ed25519.lisp ├── dsa.lisp ├── ecdsa-nistp384.lisp ├── ecdsa-nistp521.lisp ├── rsa.lisp ├── ecdsa-nistp256.lisp ├── public-key.lisp ├── private-key.lisp └── cert-key.lisp └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Emacs backup files 2 | *~ 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | github: [dnaeon] 3 | -------------------------------------------------------------------------------- /papers/fips186-2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnaeon/cl-ssh-keys/HEAD/papers/fips186-2.pdf -------------------------------------------------------------------------------- /papers/validation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnaeon/cl-ssh-keys/HEAD/papers/validation.pdf -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := test 2 | 3 | test: 4 | ./scripts/run-tests.sh 5 | 6 | .PHONY: test 7 | -------------------------------------------------------------------------------- /papers/drunken_bishop.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnaeon/cl-ssh-keys/HEAD/papers/drunken_bishop.pdf -------------------------------------------------------------------------------- /papers/ed25519-20110926.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnaeon/cl-ssh-keys/HEAD/papers/ed25519-20110926.pdf -------------------------------------------------------------------------------- /t/test-keys/id_ed25519.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIISdJ1S9+e+MY70EibBdxOeDu05ySJpi5689PxAXR7PT john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_ed25519_key_type_mismatch.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAC3NzaC1lZDI1NTE5AAAAIBQwkfbgud+SZnzZLDD35ptNfYivY7FPZyM/vBkNJWyT john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_ed25519_encrypted_3des-cbc.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID9u4qzNSB2XP3XnblSqoAQGBb4GgLSZuf+rQXZmRzU3 john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_ed25519_encrypted_aes128-ctr.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGOrPxBS1rXXOSdpUiGMSgKFHLitJVEn1ONfiGom1LCT john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_ed25519_encrypted_aes192-cbc.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF1GKH2JeafCvBxFgl35MwwM2BQvPI6tzNB7uBY/PQ7t john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_ed25519_encrypted_aes192-ctr.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPVuD7O8qWWHLXzke7Y4vyz4KUlqgbk2927xb1dtCzQv john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_ed25519_encrypted_aes256-cbc.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM/BD8eQjcwtTHvakLfaNTtqr+rg+uQonJrZqHlVaPCE john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_ed25519_encrypted_aes256-ctr.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIImY0rPbAsD4VLcazjfR72XNLScd/oiqYHWwNJ3kXw/t john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_ecdsa_nistp256.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOIn3OpgR1oVyW2U9XbrCWBmxMEHTv0/R8khYSLMqGURvm/RzoEqS58pmzY7rZkX5Q+lY1eshSCtem/X8Y93zHE= john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_ecdsa_nistp384.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBGGrerjxTKlsRvlsQsEhaDkzGNK8SNV94ba/zuY7M4zPx1gSjtlXOeJiygYIAjU1nvFZ13rbnAAWoHOHqTesVmOMeZmHJspbpCNjWdFRpDD56d33brLk0nZ/78tADN6USQ== john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_rsa_1024.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+01vcxLFk7HGvUFlFFEL0C+5ZJIj+QOhmbey43u2ooKpY/bkjV5lZ0zXbzoNKo5HgieHrf38P9ICZAd0COlK840wF7XYY9kpzW/ei5ZCWrJjnUjCGhNW1zinT270ZrYVJ13UcqhDf6/q2hu4c5HY9zXT6Aqzh2qJAI+DZ1aF5Lw== john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_ecdsa_nistp521.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAF2dKe+bG2Gl0Bhpher3OxjruX5Gdycx8CqfFyZTOcQgbO0VPOq027mZ3DlxRzvdU8DBidCFg9a7vlwqVgtSDF7zAE2GTg4YfAT9EGMrzrbIihcmNM0RB5BKzi96WReUqM/NzupVKJhBd0vMSJP7s73LiYwOIkTXL0Xxbc4u7G40MkfCg== john.doe@localhost 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM clfoundation/sbcl:2.2.4 2 | 3 | ENV QUICKLISP_ADD_TO_INIT_FILE=true 4 | ENV QUICKLISP_DIST_VERSION=latest 5 | 6 | WORKDIR /app 7 | COPY . . 8 | 9 | RUN mkdir -p ~/.config/common-lisp/source-registry.conf.d && \ 10 | echo '(:tree "/app/")' > ~/.config/common-lisp/source-registry.conf.d/workspace.conf && \ 11 | /usr/local/bin/install-quicklisp 12 | 13 | ENTRYPOINT ["./scripts/run-tests.sh"] 14 | 15 | -------------------------------------------------------------------------------- /Dockerfile.ccl: -------------------------------------------------------------------------------- 1 | FROM clfoundation/ccl:1.12 2 | 3 | ENV QUICKLISP_ADD_TO_INIT_FILE=true 4 | ENV QUICKLISP_DIST_VERSION=latest 5 | ENV LISP=ccl 6 | 7 | WORKDIR /app 8 | COPY . . 9 | 10 | RUN mkdir -p ~/.config/common-lisp/source-registry.conf.d && \ 11 | echo '(:tree "/app/")' > ~/.config/common-lisp/source-registry.conf.d/workspace.conf && \ 12 | /usr/local/bin/install-quicklisp 13 | 14 | ENTRYPOINT ["./scripts/run-tests.sh"] 15 | -------------------------------------------------------------------------------- /Dockerfile.ecl: -------------------------------------------------------------------------------- 1 | FROM clfoundation/ecl:21.2.1 2 | 3 | ENV QUICKLISP_ADD_TO_INIT_FILE=true 4 | ENV QUICKLISP_DIST_VERSION=latest 5 | ENV LISP=ecl 6 | 7 | WORKDIR /app 8 | COPY . . 9 | 10 | RUN mkdir -p ~/.config/common-lisp/source-registry.conf.d && \ 11 | echo '(:tree "/app/")' > ~/.config/common-lisp/source-registry.conf.d/workspace.conf && \ 12 | /usr/local/bin/install-quicklisp 13 | 14 | ENTRYPOINT ["./scripts/run-tests.sh"] 15 | -------------------------------------------------------------------------------- /Dockerfile.sbcl: -------------------------------------------------------------------------------- 1 | FROM clfoundation/sbcl:2.2.4 2 | 3 | ENV QUICKLISP_ADD_TO_INIT_FILE=true 4 | ENV QUICKLISP_DIST_VERSION=latest 5 | ENV LISP=sbcl 6 | 7 | WORKDIR /app 8 | COPY . . 9 | 10 | RUN mkdir -p ~/.config/common-lisp/source-registry.conf.d && \ 11 | echo '(:tree "/app/")' > ~/.config/common-lisp/source-registry.conf.d/workspace.conf && \ 12 | /usr/local/bin/install-quicklisp 13 | 14 | ENTRYPOINT ["./scripts/run-tests.sh"] 15 | -------------------------------------------------------------------------------- /t/test-keys/id_ed25519: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACCEnSdUvfnvjGO9BImwXcTng7tOckiaYuevPT8QF0ez0wAAAJhyMxiacjMY 4 | mgAAAAtzc2gtZWQyNTUxOQAAACCEnSdUvfnvjGO9BImwXcTng7tOckiaYuevPT8QF0ez0w 5 | AAAECqt0chM4BrBefEWR94jEup6QZ3rk6UiH1Hr2wzhKxe5oSdJ1S9+e+MY70EibBdxOeD 6 | u05ySJpi5689PxAXR7PTAAAAEmpvaG4uZG9lQGxvY2FsaG9zdAECAw== 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /t/test-keys/id_ed25519_key_type_mismatch: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACAUMJH24LnfkmZ82Sww9+abTX2Ir2OxT2cjP7wZDSVskwAAAJirhPYUq4T2 4 | FAAAAAtzc2gtZWQyNTUxOQAAACAUMJH24LnfkmZ82Sww9+abTX2Ir2OxT2cjP7wZDSVskw 5 | AAAED5lAf6yz4mPdjakxkFw9GeP5cZ/zqcB5acvKLxs2JIQxQwkfbgud+SZnzZLDD35ptN 6 | fYivY7FPZyM/vBkNJWyTAAAAEmpvaG4uZG9lQGxvY2FsaG9zdAECAw== 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /t/test-keys/id_ed25519_encrypted_3des-cbc: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAACDNkZXMtY2JjAAAABmJjcnlwdAAAABgAAAAQftSN+2fAN0 3 | kXI4fi/QV8uQAAABAAAAABAAAAMwAAAAtzc2gtZWQyNTUxOQAAACA/buKszUgdlz91525U 4 | qqAEBgW+BoC0mbn/q0F2Zkc1NwAAAJiEJS5abQxucA18iBbADGAkGTkXdkeSwVC6uAXv+P 5 | 1H8dwSHl9DiiSz46yoAWQVqqssbdfdSQOGkUMmbhXxDB0B74bMBfynItEk0mXT4EIkyGGe 6 | V7A77yIVgOSNDqCE0dQgv+iKg0mGKw34guWq2778L9NrlC8ujjAZ32vIs2CIetaGU3u5Zi 7 | jMOa8uQNw4DCVvKPjWZ5pFMw== 8 | -----END OPENSSH PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /t/test-keys/id_ed25519_encrypted_aes128-ctr: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAACmFlczEyOC1jdHIAAAAGYmNyeXB0AAAAGAAAABApe2YKxt 3 | qT7/PpY9d5JwGhAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIGOrPxBS1rXXOSdp 4 | UiGMSgKFHLitJVEn1ONfiGom1LCTAAAAoFU5W/90oVrfSJbNY+UnqI2JhxDFhnO/vPIE+L 5 | J8v4ZCibsTc8VNp959XH0yngOB+bhJS90URgwe3n3Yj3k7IWjtfAbfg1mJ8EwCbc5Ete75 6 | y/PnDCagGUOB+T/Z7KGHQFiqEQJppeLcI/b8/MdNRonurlnCtP2vSWxeMgEcYfbx/+C+Gb 7 | hatQOBgJ6NAjOEDfGt2R1RXBVhIeLO9iAAaM4= 8 | -----END OPENSSH PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /t/test-keys/id_ed25519_encrypted_aes192-cbc: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAACmFlczE5Mi1jYmMAAAAGYmNyeXB0AAAAGAAAABDmGVI7dP 3 | VydaS5dFVrwCxjAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIF1GKH2JeafCvBxF 4 | gl35MwwM2BQvPI6tzNB7uBY/PQ7tAAAAoJ69I5nQFO/7zmWq4Op1uO+7pBwhxWAeVVpFxr 5 | Y9SvBU8AeQrgZscMQ1478LQ0ugltHCtIE3A+yQrizhz8vy1T8lrqyPm5DxCJykkWOyu8wK 6 | Y1a9VwbgqL6nF0G8VdFY2ygB5D/c7yZ3UVicEEol18DL2uLoL8BpFrj9YL+BACEfgTOOPP 7 | gopzPZJQnpb3g9y+Trk6C9TuQCDulmxl3oT0s= 8 | -----END OPENSSH PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /t/test-keys/id_ed25519_encrypted_aes192-ctr: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAACmFlczE5Mi1jdHIAAAAGYmNyeXB0AAAAGAAAABBB0ivqr2 3 | T8W0TAo7GlNoJCAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIPVuD7O8qWWHLXzk 4 | e7Y4vyz4KUlqgbk2927xb1dtCzQvAAAAoL7bOw2e3r+gufqmenLZSQU+pNcUb4sW61iX8y 5 | o877Nz783em5unBxPFQTjsxFYfcBXuPZZmDB5Atnyd+Qwg5SQ0/yKKkmPanEWeFCNvlSZq 6 | orsE4szxOuqq8V/fTPS/ReqUKcb/rIm/i0Vd7KG2E8lyzKZaqvHsoJnG0VXbFnnE2d4IV0 7 | oBoj+ifN2papTAHCH3B5hJMxbTJFSKaDOEtb8= 8 | -----END OPENSSH PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /t/test-keys/id_ed25519_encrypted_aes256-cbc: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABAe0BO0W4 3 | Eb86CRPbUS5I2HAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIM/BD8eQjcwtTHva 4 | kLfaNTtqr+rg+uQonJrZqHlVaPCEAAAAoF3tCqjz1ro1zuLeOrY0alCLqw5xl6aMmg0uYG 5 | dufBmzjGGl6J32vyArTx01vhtQZuRvFNWeIEF5Tvr2k3S5ZjoEmd54AD9P56uXrV1M4Hsb 6 | Q2tLFNV69pXzuI1o7ReOaPjO5kX65hh1fLJ7gCLs6hX39Qe3NNcXUM9d1ueRlEFxBzKzPr 7 | M4DEWFQzVlzkAUwJBdz67J5xFTTq77SeKaJhY= 8 | -----END OPENSSH PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /t/test-keys/id_ed25519_encrypted_aes256-ctr: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBi35Be2e 3 | jVKytEsOX+hlhQAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIImY0rPbAsD4VLca 4 | zjfR72XNLScd/oiqYHWwNJ3kXw/tAAAAoGpOmlaI7pDGWjYziVdWAHLSOni7hhqgNXDQ0o 5 | 7xdW8VW1+WXO9rMU+TVqLiQYXKSQiKd/OkCLtkj0n0Y53KCpqyNYK6m6EeVaF1fqh1OIRt 6 | jFHik9GFKL2WWGZ5B+0nRvRoGTEPpKzNgMoNSVVbMPKa9uMgQurQkjwZ+JqIJR8K/9IYiu 7 | 1kBBqvjBhHgSUAIwqDQzCk73iMcpaQ7mqhWYE= 8 | -----END OPENSSH PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /t/test-keys/id_ecdsa_nistp256: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS 3 | 1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTiJ9zqYEdaFcltlPV26wlgZsTBB079 4 | P0fJIWEizKhlEb5v0c6BKkufKZs2O62ZF+UPpWNXrIUgrXpv1/GPd8xxAAAAsIKUwcqClM 5 | HKAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOIn3OpgR1oVyW2U 6 | 9XbrCWBmxMEHTv0/R8khYSLMqGURvm/RzoEqS58pmzY7rZkX5Q+lY1eshSCtem/X8Y93zH 7 | EAAAAgRzuv3ITJvOzlrlTq1wHhXE9pKv5qOpeQBtLpF4NARCEAAAASam9obi5kb2VAbG9j 8 | YWxob3N0AQIDBAUG 9 | -----END OPENSSH PRIVATE KEY----- 10 | -------------------------------------------------------------------------------- /t/test-keys/id_rsa_3072.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCsngzCcay+lQ+34qUeUSH2m1ZYW9B0a2rxpMmvYFcOyL/hRPJwv8XO89T0+HQIZRC+xlM3BSqdFGs+B58MYXPvo3H+p00CJN8tUjvC3VD74kiXSNxIyhBpKCY1s58RxnWS/6bPQIYfnCVBiQZnkNe1T3isxND1Y71TnbSz5QN2xBkAtiGPH0dPM89yWbZpTjTCaIOfyZn2fBBsmp0zUgEJ7o9W9Lrxs1f0Pn+bZ4PqFSEUzlub7mAQ+RpwgGeLeWIFz+o6KQJPFiuRgzQU6ZsY+wjorVefzgeqpRiWGw/bEyUDck09B4B0IWoTtIiKRzd635nOo7Lz/1XgaMZ60WZD9T/labEWcKmtp4Y7NoCkep0DyYyoAgWrco4FD1r0g4WcVbsJQt8HzRy9UaHlh6YPY/xkk0bSiljpygEiT48FxniqE+6HY+7SbC1wz5QThY+UsIiDgFcg3BljskfT8Il3hateXI2wEXqww4+a+DxcHzypclYorbQKUzdzNLZRBNk= john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_rsa_missing_key_type.pub: -------------------------------------------------------------------------------- 1 | AAAAB3NzaC1yc2EAAAADAQABAAABgQCsngzCcay+lQ+34qUeUSH2m1ZYW9B0a2rxpMmvYFcOyL/hRPJwv8XO89T0+HQIZRC+xlM3BSqdFGs+B58MYXPvo3H+p00CJN8tUjvC3VD74kiXSNxIyhBpKCY1s58RxnWS/6bPQIYfnCVBiQZnkNe1T3isxND1Y71TnbSz5QN2xBkAtiGPH0dPM89yWbZpTjTCaIOfyZn2fBBsmp0zUgEJ7o9W9Lrxs1f0Pn+bZ4PqFSEUzlub7mAQ+RpwgGeLeWIFz+o6KQJPFiuRgzQU6ZsY+wjorVefzgeqpRiWGw/bEyUDck09B4B0IWoTtIiKRzd635nOo7Lz/1XgaMZ60WZD9T/labEWcKmtp4Y7NoCkep0DyYyoAgWrco4FD1r0g4WcVbsJQt8HzRy9UaHlh6YPY/xkk0bSiljpygEiT48FxniqE+6HY+7SbC1wz5QThY+UsIiDgFcg3BljskfT8Il3hateXI2wEXqww4+a+DxcHzypclYorbQKUzdzNLZRBNk= john.doe@localhost 2 | -------------------------------------------------------------------------------- /scripts/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | LISP=${LISP:-sbcl} 6 | 7 | _no_debug_flag="" 8 | case "${LISP}" in 9 | sbcl) 10 | _no_debug_flag="--non-interactive" 11 | ;; 12 | ecl) 13 | _no_debug_flag="--nodebug" 14 | ;; 15 | esac 16 | 17 | ${LISP} ${_no_debug_flag} \ 18 | --eval '(ql:quickload :cl-ssh-keys.test)' \ 19 | --eval '(setf rove:*enable-colors* nil)' \ 20 | --eval '(asdf:test-system :cl-ssh-keys.test)' \ 21 | --eval '(uiop:quit (length (rove/core/stats:all-failed-assertions rove/core/stats:*stats*)))' 22 | -------------------------------------------------------------------------------- /t/test-keys/id_rsa_3072_encrypted_aes128-cbc.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC9xiClmWcsYs/QR1JiCIXfLBDjVwWeHg+nGgrvZJBk/fVsUvHyMnYze/vgY+261qd0MAP0UFLF3wvVo/FRJKpSWCvTvDnzfOxMso2UVJcbWmeTc2ygV3rXjv3bNm/5XFX86b3vr5pWZ55dw9v+7hYTJEUMLifnDNoQcVxhwInamcKByNZad2h0A+T8lmyq2+hfaECNdZm35eZ9cR+Xj9Vn7LnE4knGnjadPA5wyqBWEpHVOllJozlJwBw1ba2YtZZlY4y+xb52RftGbnBSk5TOCp8G5P5gXFkM4oN90kevO3FCfPgXloey5Y2znPCJbgRjjsjJLzKHTATQjcrOqdz8FsmCSsBCq20A/9E0RvwbvdGmPyE5SyAzGwrRVWBwAU543sFgZLvEw17dttHNxH7j+UngcUxzzQ3GkI9Bmiy4EXGlUSCqYNjFVp2Vo4EUjou+7dcPWzVrfPoI3OnU00GBtikA7oibhaafd0NpZq1LbhblCZznzNGkVzret1Pa6/k= john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_rsa_3072_host_ca.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8pT4fwWRywsVB6oMttjMkv5LoouWeDIun7JVSdcZMCs9xa2RxnTQjft4HksCNxx4qyUuCrhScgvvGgBo0HeDJmhYoLjyvcDm2ljk9bJhFuN1ER4lzHIb0ZFi7Wvo65Jym//eS6vr0tXI1EmlZhIiYDH3qIKILGQpEY8AIe8Ywgotsv0IVC+sa87NUZLF7yjq9zVTweaAkU03y3+ar3un9Y29b6x3+xYX7CJMWE5BQtlQXJnVKj2h5s90l7haA+lilosBfwRzkjy/shTVNCXWxzyUwKQa7zvhgQ+rWIs8zFhV+P05xXxfKwhMsD9aS2owxLJEbG96HxpeC+kR8vyHMPUemoGU1jjIbL1jNMktVpIL5Gk+HEfVvjzCoNwt0DoAh+XcTItd4Z/l3iEG72rqfKiL8lKR61ik1u3HDBOfV/bxzd+J/EpIdMbk7NMKRkW2Ji4B5KAmWDTc2iwqkG7+PhyXdIShPJVjx/5YGO5aJ9GdrcZtZiwulZeinSp3f/Us= mnikolov@mnikolov-a03.vmware.com 2 | -------------------------------------------------------------------------------- /t/test-keys/id_rsa_3072_user_ca.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQD0uTi6pH1egavOeaXVBoeeS6lj2hkxEywspW27KmnAvBBeii21lOULzWR90gQuD3WRE+9cfdf5HBp3Cim2ePlMAArpNmZXm4S4ya2VZqumB5JNkK4SyEGKSKQ7dLal2dZPJNfWOrLEm2NtVfnpDS5iTxvaRGsXr3w0egiwlWfGfY9cfiegPZSTgaUjPVO+7KhnF8p+m8VOgUJQZklfgLjzxeswryhRNhFS9LUJpEZFkqtg+phFkVmDX2ZzxQ4PGCjHoliIvBpHjILGe6f8QP8jIcgF2+lu0LPBYF8A66OaKPCUZJIM8PsWivCyToK2k0/Mnk74+Y9AlEtEO5BFIAmxrh9tnySU3g2BXQqKbcFqV1iYHQuQJrXdU42uyPRp6ItPpPkhGwlT/750Wv9z19KK81ikAvGpqIVB5Nm8GCdEa+NrwTe9/ghz5tUoEZXlz0fZ45JkA9+lzWhIMUmSnIgp5pjr1UZHiWoRtDUQemmIfh6/pT1l6v3poH7stMTI4EM= mnikolov@mnikolov-a03.vmware.com 2 | -------------------------------------------------------------------------------- /t/test-keys/id_rsa_unknown_key_type.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa-no-such-type AAAAB3NzaC1yc2EAAAADAQABAAABgQDORE13WOiQo7uecpdTXIwqTB3fOK3W6Pt0qbLyJcAn75kshyplS33Mf3+6og/mvot7FO4qKfQlqI6dUQcQKl8w/XjTAAOChNfq+dgwNrj8rYeNcaVK+BejYtHusBzgGOOqxYCTncOBo4scze4jzCh5lTR2+BveiUfNB0dqxYbHYMurRppg7zYyyVcXQrArI2k/COaiabh5SAPl2M8Q9EQu1McnjADvL4wFrSP6LWd92jHSzrn612PtycOfORYPPSeKzA13yNZ0BVMAUI8jIZ+9BXrmeFVsSx9/nNRSB5xXnq/54PyeDR4e8UDHLa2tmyPxEY8J7YukHiimQTX7cJ+lcR3eAjpENl6h6TeN+ROgDUUfyY+16vpCDWos6IMq9VW0U2lkJ0xmULRlZ+DEWJib/SgGuE+hItkrYSQGEHlePoB3CRqSwiJZgcVjm+LOUwO++yiY952o5nqGe4cQ+4dDFRaj9g7RWF32k9YVijIobhRzHcZXOcmI1R3ZW701P9E= john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_dsa.pub: -------------------------------------------------------------------------------- 1 | ssh-dss AAAAB3NzaC1kc3MAAACBAMV0EY/L9PVk7aHVxW8iZ8WLUYeSjCp/cjIBazlblLRrxP9g0OJ3TRv0ER4gUWXkSU9TVMzxeeEhQMNMlztVf3flyNbar1fzOguim+93CDJkITt9kxF68XIF7dj/1T71hKEC9F5ylE0q0WekWgXAbLaNdtbRFJYIV45mzk5Mjvc7AAAAFQDKGkxsB8cBOCmcx7IysEvO+VlBOQAAAIEApSV9OsCmjbi9PL/ve0ztZbm1JPFmE77TCPh7KJYEyo/8+HG1L72STiBkaUKAbptdxxahiqXkcBpvWl2H331vRGJEJ9/Fcsg93QILimbKGM/VXTw6DJq7mwFTQUVSeFSLnQPraVPyICnnO76sPOizJMexOwmrTKCN0ipgOQYBQtcAAACBAKc7EYtdHPZcfjTT4k/4OcTcPezpoi5xYxafeyKwDTxz52uV+wha9RzjDmg5rZLmPHAAVaC8ln+rGdcZnFvWw9mFthBL9bsaEJWfPoRdntHbhuiYAsZx6cT+Xs5n+xLrGbexlEln1rNwbjYUWPW4Vu3+v3t7Ol8nGk18Ro9ILGlL john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_ecdsa_nistp384: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS 3 | 1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQRhq3q48UypbEb5bELBIWg5MxjSvEjV 4 | feG2v87mOzOMz8dYEo7ZVzniYsoGCAI1NZ7xWdd625wAFqBzh6k3rFZjjHmZhybKW6QjY1 5 | nRUaQw+end926y5NJ2f+/LQAzelEkAAADgF+RdEBfkXRAAAAATZWNkc2Etc2hhMi1uaXN0 6 | cDM4NAAAAAhuaXN0cDM4NAAAAGEEYat6uPFMqWxG+WxCwSFoOTMY0rxI1X3htr/O5jszjM 7 | /HWBKO2Vc54mLKBggCNTWe8VnXetucABagc4epN6xWY4x5mYcmylukI2NZ0VGkMPnp3fdu 8 | suTSdn/vy0AM3pRJAAAAMHWMhr5FynsxurB1oZd0T2rmYPkOC0UeW74TB0wAPvcJZgdLGX 9 | urHaLDywlyB82GuAAAABJqb2huLmRvZUBsb2NhbGhvc3QBAgMEBQY= 10 | -----END OPENSSH PRIVATE KEY----- 11 | -------------------------------------------------------------------------------- /scripts/run-ci-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Runs the CI tests 4 | # 5 | 6 | set -e 7 | 8 | SCRIPTS_DIR=$( dirname `readlink -f -- "${0}"` ) 9 | SYSTEMS_DIR=$( dirname "${SCRIPTS_DIR}" ) 10 | ASDF_SOURCE_REGISTRY=~/.config/common-lisp/source-registry.conf.d 11 | 12 | if [ -z "${CI_SYSTEM}" ]; then 13 | echo "Script does not seem to be invoked from a CI system, exiting." 14 | exit 1 15 | fi 16 | 17 | # Install Quicklisp 18 | /usr/local/bin/install-quicklisp 19 | 20 | # Configure ASDF, so that it finds our systems 21 | mkdir -p "${ASDF_SOURCE_REGISTRY}" 22 | echo "(:tree \"${SYSTEMS_DIR}\")" > "${ASDF_SOURCE_REGISTRY}/workspace.conf" 23 | 24 | ${SCRIPTS_DIR}/run-tests.sh 25 | -------------------------------------------------------------------------------- /t/test-keys/id_ecdsa_nistp521: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS 3 | 1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBdnSnvmxthpdAYaYXq9zsY67l+Rnc 4 | nMfAqnxcmUznEIGztFTzqtNu5mdw5cUc73VPAwYnQhYPWu75cKlYLUgxe8wBNhk4OGHwE/ 5 | RBjK862yIoXJjTNEQeQSs4velkXlKjPzc7qVSiYQXdLzEiT+7O9y4mMDiJE1y9F8W3OLux 6 | uNDJHwoAAAEQhFfYg4RX2IMAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ 7 | AAAIUEAXZ0p75sbYaXQGGmF6vc7GOu5fkZ3JzHwKp8XJlM5xCBs7RU86rTbuZncOXFHO91 8 | TwMGJ0IWD1ru+XCpWC1IMXvMATYZODhh8BP0QYyvOtsiKFyY0zREHkErOL3pZF5Soz83O6 9 | lUomEF3S8xIk/uzvcuJjA4iRNcvRfFtzi7sbjQyR8KAAAAQgEYuMA+bHU0gB+Xs2SqlGvx 10 | 7m2tsjb3sQ1o4uSlYb6W3IoTCFCKduLUSOM7iiqGwtxofXIBUq4hro1HAu7flUKKDwAAAB 11 | Jqb2huLmRvZUBsb2NhbGhvc3Q= 12 | -----END OPENSSH PRIVATE KEY----- 13 | -------------------------------------------------------------------------------- /cl-ssh-keys.test.asd: -------------------------------------------------------------------------------- 1 | (defpackage cl-ssh-keys-test-system 2 | (:use :cl :asdf)) 3 | (in-package :cl-ssh-keys-test-system) 4 | 5 | (defsystem "cl-ssh-keys.test" 6 | :name "cl-ssh-keys.test" 7 | :long-name "cl-ssh-keys.test" 8 | :description "Test suite for cl-ssh-keys system" 9 | :version "0.5.0" 10 | :author "Marin Atanasov Nikolov " 11 | :maintainer "Marin Atanasov Nikolov " 12 | :license "BSD 2-Clause" 13 | :homepage "https://github.com/dnaeon/cl-ssh-keys" 14 | :bug-tracker "https://github.com/dnaeon/cl-ssh-keys" 15 | :source-control "https://github.com/dnaeon/cl-ssh-keys" 16 | :depends-on (:cl-ssh-keys 17 | :alexandria 18 | :rove) 19 | :components ((:module "tests" 20 | :pathname #P"t/" 21 | :components ((:file "test-suite")))) 22 | :perform (test-op (op c) (uiop:symbol-call :rove :run-suite :cl-ssh-keys.test))) 23 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yaml: -------------------------------------------------------------------------------- 1 | # .github/workflows/run-tests.yaml 2 | name: test 3 | on: [ push, pull_request ] 4 | jobs: 5 | run-tests: 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | implementation: 10 | - sbcl 11 | - ecl 12 | - ccl 13 | include: 14 | - implementation: sbcl 15 | image: clfoundation/sbcl:2.2.4 16 | command: sbcl 17 | - implementation: ecl 18 | image: clfoundation/ecl:21.2.1 19 | command: ecl 20 | - implementation: ccl 21 | image: clfoundation/ccl:1.12 22 | command: ccl 23 | runs-on: ubuntu-latest 24 | container: 25 | image: ${{ matrix.image }} 26 | env: 27 | CI_SYSTEM: github 28 | QUICKLISP_ADD_TO_INIT_FILE: true 29 | QUICKLISP_DIST_VERSION: latest 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@v3 33 | - name: Run tests 34 | run: | 35 | env LISP=${{ matrix.command }} ./scripts/run-ci-tests.sh 36 | -------------------------------------------------------------------------------- /t/test-keys/id_rsa_1024: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn 3 | NhAAAAAwEAAQAAAIEAvtNb3MSxZOxxr1BZRRRC9AvuWSSI/kDoZm3suN7tqKCqWP25I1eZ 4 | WdM1286DSqOR4Inh639/D/SAmQHdAjpSvONMBe12GPZKc1v3ouWQlqyY51IwhoTVtc4p09 5 | u9Ga2FSdd1HKoQ3+v6tobuHOR2Pc10+gKs4dqiQCPg2dWheS8AAAIIGX2Lqhl9i6oAAAAH 6 | c3NoLXJzYQAAAIEAvtNb3MSxZOxxr1BZRRRC9AvuWSSI/kDoZm3suN7tqKCqWP25I1eZWd 7 | M1286DSqOR4Inh639/D/SAmQHdAjpSvONMBe12GPZKc1v3ouWQlqyY51IwhoTVtc4p09u9 8 | Ga2FSdd1HKoQ3+v6tobuHOR2Pc10+gKs4dqiQCPg2dWheS8AAAADAQABAAAAgDKEtf8eO3 9 | BigD5PYTK+iHMfbXxABdx5xozHcTIIIpbP4gkpMog+uqRjHBES/QMXhpkRJ7Q2zqakrh7T 10 | tXTPh/Nv137EryTC2zaFd9Qg+UHEPyUc72tPmt/93iX6DjgnJ7ktdz65eP+CNMPaWPqbVz 11 | Ub4xQmieBnMpZkJpOt+zVhAAAAQBLrcuzfc92Mf3LBSxdEM31pW+gqNa4Jo1MeabX1Kz5Y 12 | H/1OV51N5G23MlGpCU2dTjf89Er3rq0cQD8THpYpWc8AAABBAPezgRtqbsx3/4teUSKcyI 13 | 9rG8x//AHPZqXoYuwHQ7nXttmHwoE+A6UAmLjyWDLX365UgU2i7F0NcCSczjfxkZkAAABB 14 | AMU4CpZJ9/tBa2T4b7PSn/ewCE/TaxkUqDUt0bn4M2tm+yyqNsQrOotRfrMHSu40oFsHkH 15 | 96SjpIn56Ed+jlLgcAAAASam9obi5kb2VAbG9jYWxob3N0AQ== 16 | -----END OPENSSH PRIVATE KEY----- 17 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yaml: -------------------------------------------------------------------------------- 1 | # .github/workflows/docker-image.yaml 2 | # 3 | # See [1] and [2] for more details 4 | # 5 | # [1]: https://github.com/marketplace/actions/build-and-push-docker-images 6 | # [2]: https://github.com/docker/buildx 7 | name: docker-image 8 | on: [ push, pull_request ] 9 | jobs: 10 | docker-image: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | implementation: 15 | - sbcl 16 | - ecl 17 | - ccl 18 | include: 19 | - implementation: sbcl 20 | dockerfile: Dockerfile.sbcl 21 | - implementation: ecl 22 | dockerfile: Dockerfile.ecl 23 | - implementation: ccl 24 | dockerfile: Dockerfile.ccl 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v2 29 | - name: Build Docker image - ${{ matrix.implementation }} 30 | uses: docker/build-push-action@v3 31 | with: 32 | push: false 33 | file: ${{ matrix.dockerfile }} 34 | tags: dnaeon/cl-ssh-keys:${{ matrix.implementation }}-latest 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Marin Atanasov Nikolov 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer 10 | in this position and unchanged. 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /t/test-keys/id_dsa: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABswAAAAdzc2gtZH 3 | NzAAAAgQDFdBGPy/T1ZO2h1cVvImfFi1GHkowqf3IyAWs5W5S0a8T/YNDid00b9BEeIFFl 4 | 5ElPU1TM8XnhIUDDTJc7VX935cjW2q9X8zoLopvvdwgyZCE7fZMRevFyBe3Y/9U+9YShAv 5 | RecpRNKtFnpFoFwGy2jXbW0RSWCFeOZs5OTI73OwAAABUAyhpMbAfHATgpnMeyMrBLzvlZ 6 | QTkAAACBAKUlfTrApo24vTy/73tM7WW5tSTxZhO+0wj4eyiWBMqP/PhxtS+9kk4gZGlCgG 7 | 6bXccWoYql5HAab1pdh999b0RiRCffxXLIPd0CC4pmyhjP1V08Ogyau5sBU0FFUnhUi50D 8 | 62lT8iAp5zu+rDzosyTHsTsJq0ygjdIqYDkGAULXAAAAgQCnOxGLXRz2XH400+JP+DnE3D 9 | 3s6aIucWMWn3sisA08c+drlfsIWvUc4w5oOa2S5jxwAFWgvJZ/qxnXGZxb1sPZhbYQS/W7 10 | GhCVnz6EXZ7R24bomALGcenE/l7OZ/sS6xm3sZRJZ9azcG42FFj1uFbt/r97ezpfJxpNfE 11 | aPSCxpSwAAAfDz15I389eSNwAAAAdzc2gtZHNzAAAAgQDFdBGPy/T1ZO2h1cVvImfFi1GH 12 | kowqf3IyAWs5W5S0a8T/YNDid00b9BEeIFFl5ElPU1TM8XnhIUDDTJc7VX935cjW2q9X8z 13 | oLopvvdwgyZCE7fZMRevFyBe3Y/9U+9YShAvRecpRNKtFnpFoFwGy2jXbW0RSWCFeOZs5O 14 | TI73OwAAABUAyhpMbAfHATgpnMeyMrBLzvlZQTkAAACBAKUlfTrApo24vTy/73tM7WW5tS 15 | TxZhO+0wj4eyiWBMqP/PhxtS+9kk4gZGlCgG6bXccWoYql5HAab1pdh999b0RiRCffxXLI 16 | Pd0CC4pmyhjP1V08Ogyau5sBU0FFUnhUi50D62lT8iAp5zu+rDzosyTHsTsJq0ygjdIqYD 17 | kGAULXAAAAgQCnOxGLXRz2XH400+JP+DnE3D3s6aIucWMWn3sisA08c+drlfsIWvUc4w5o 18 | Oa2S5jxwAFWgvJZ/qxnXGZxb1sPZhbYQS/W7GhCVnz6EXZ7R24bomALGcenE/l7OZ/sS6x 19 | m3sZRJZ9azcG42FFj1uFbt/r97ezpfJxpNfEaPSCxpSwAAABRqqx3dZVeTnfAOipsqYyu4 20 | GKfkFQAAABJqb2huLmRvZUBsb2NhbGhvc3QBAgMEBQYH 21 | -----END OPENSSH PRIVATE KEY----- 22 | -------------------------------------------------------------------------------- /t/test-keys/id_ed25519-cert.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAICmiR2q5x8K+tzkqqWiE3eFvMQJLMOA0wLk6zUG9AZgjAAAAIISdJ1S9+e+MY70EibBdxOeDu05ySJpi5689PxAXR7PTAAAAAAAAAAAAAAABAAAACGpvaG4uZG9lAAAAAAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAABlwAAAAdzc2gtcnNhAAAAAwEAAQAAAYEA9Lk4uqR9XoGrznml1QaHnkupY9oZMRMsLKVtuyppwLwQXoottZTlC81kfdIELg91kRPvXH3X+Rwadwoptnj5TAAK6TZmV5uEuMmtlWarpgeSTZCuEshBikikO3S2pdnWTyTX1jqyxJtjbVX56Q0uYk8b2kRrF698NHoIsJVnxn2PXH4noD2Uk4GlIz1TvuyoZxfKfpvFToFCUGZJX4C488XrMK8oUTYRUvS1CaRGRZKrYPqYRZFZg19mc8UODxgox6JYiLwaR4yCxnun/ED/IyHIBdvpbtCzwWBfAOujmijwlGSSDPD7Forwsk6CtpNPzJ5O+PmPQJRLRDuQRSAJsa4fbZ8klN4NgV0Kim3BaldYmB0LkCa13VONrsj0aeiLT6T5IRsJU/++dFr/c9fSivNYpALxqaiFQeTZvBgnRGvja8E3vf4Ic+bVKBGV5c9H2eOSZAPfpc1oSDFJkpyIKeaY69VGR4lqEbQ1EHppiH4ev6U9Zer96aB+7LTEyOBDAAABlAAAAAxyc2Etc2hhMi01MTIAAAGAT9cu8U1JlTHdzbqwj0Pa7LalP7S04X6vpXnJrENjvdCchibF/uAXtAtCUWb6QJvMaC+dINLOGM9FhN7QxvHLf6F/kRLuG723CaAWggKJ71NVVMGuTfjTgJ8aeyUMqpkhJnmicx30pw3vREdiv9X8Z6eG8V7VdAF+nXS6VQUa4i+2K/l3R707l4jltqe04f/KYU69l668DM8iFfChPWg/LBd7noHB7HYu7z4o3r9XyqTg+vy2m/epQDTrXjKOeZs0omsHjeYX8VGBOSHCXRTlhaiMPZMIjpzoyWAfWTnykIpeOJuwr4o9OTDIYIuPf8oxfXrUFTFb2OLHIoJpz/RvB1ySuSN/n9E59gV/tLROJk8Q9mYAL/iDpgvZ4ljIMHi/OMi563DytVp7xIyDjXTlId2SpuLetvX4CSkl0ewjyu7uYx06jsM8sXO+8Evs6Ci07AYRoGIqZUvlPTm9c2UyWsaL40fzZfk7yu9WCpakH+np3XhPjZsyVLPcpfwMwgYy john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_ecdsa_nistp256-cert.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgp9vUV8QKoLOxBvmHKNl/VZk7CAtfBekvA/pRLjzDL6sAAAAIbmlzdHAyNTYAAABBBOIn3OpgR1oVyW2U9XbrCWBmxMEHTv0/R8khYSLMqGURvm/RzoEqS58pmzY7rZkX5Q+lY1eshSCtem/X8Y93zHEAAAAAAAAAAAAAAAEAAAAIam9obi5kb2UAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQD0uTi6pH1egavOeaXVBoeeS6lj2hkxEywspW27KmnAvBBeii21lOULzWR90gQuD3WRE+9cfdf5HBp3Cim2ePlMAArpNmZXm4S4ya2VZqumB5JNkK4SyEGKSKQ7dLal2dZPJNfWOrLEm2NtVfnpDS5iTxvaRGsXr3w0egiwlWfGfY9cfiegPZSTgaUjPVO+7KhnF8p+m8VOgUJQZklfgLjzxeswryhRNhFS9LUJpEZFkqtg+phFkVmDX2ZzxQ4PGCjHoliIvBpHjILGe6f8QP8jIcgF2+lu0LPBYF8A66OaKPCUZJIM8PsWivCyToK2k0/Mnk74+Y9AlEtEO5BFIAmxrh9tnySU3g2BXQqKbcFqV1iYHQuQJrXdU42uyPRp6ItPpPkhGwlT/750Wv9z19KK81ikAvGpqIVB5Nm8GCdEa+NrwTe9/ghz5tUoEZXlz0fZ45JkA9+lzWhIMUmSnIgp5pjr1UZHiWoRtDUQemmIfh6/pT1l6v3poH7stMTI4EMAAAGUAAAADHJzYS1zaGEyLTUxMgAAAYDcNs+HHrINPl6HL+YRc7rXbDfPHr3m7ht48OMp6YgiUUuBTDVrKeu788Wq3LeiwWYPASgb5Q5b66rzA7wmY52OjKZ6jzSOoWxjn8adx5764eGA0dRoUhcaAh6xeylaXWRdpECTa8iAp8mM/H8O3zL7qPFctbX9C9gpycP9ZkKswJl+30wVy8xb7tAiodMkYUaenu+pg7Hq+ADYbAnMPm/b5tDtIluTrHpbmWgsYlHLnZuNITdNQo9dady1ETm3aXLWYcp+eu7dVtekHb23VM2YRbbc/f5esjtduUjP8gSuduTAD+tvYeHsMT82DOcGLFKyXg+8QMkGqEopbxfaFtgvudYoz/1+MJeXCkWK7SiabFpq55cl5qhf+2Gs+SqoKluZ/gLQqZGCc63mBaKaPkhpBzi10xGGM3ZEQUTTT+dF2a4y77kpS6KepSdRn/ALV1RFJYaHMAAeYvKt+SpOAZKB5kdorrU1QDmAfxBPtxgNW1Am5QmCv+Npfp5fcddfM3M= john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_ecdsa_nistp384-cert.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp384-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAzODQtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgxymu2BCXPf54Nxtbn+6peasidCHPYBTcQaa4hhqP44kAAAAIbmlzdHAzODQAAABhBGGrerjxTKlsRvlsQsEhaDkzGNK8SNV94ba/zuY7M4zPx1gSjtlXOeJiygYIAjU1nvFZ13rbnAAWoHOHqTesVmOMeZmHJspbpCNjWdFRpDD56d33brLk0nZ/78tADN6USQAAAAAAAAAAAAAAAQAAAAhqb2huLmRvZQAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAPS5OLqkfV6Bq855pdUGh55LqWPaGTETLCylbbsqacC8EF6KLbWU5QvNZH3SBC4PdZET71x91/kcGncKKbZ4+UwACuk2ZlebhLjJrZVmq6YHkk2QrhLIQYpIpDt0tqXZ1k8k19Y6ssSbY21V+ekNLmJPG9pEaxevfDR6CLCVZ8Z9j1x+J6A9lJOBpSM9U77sqGcXyn6bxU6BQlBmSV+AuPPF6zCvKFE2EVL0tQmkRkWSq2D6mEWRWYNfZnPFDg8YKMeiWIi8GkeMgsZ7p/xA/yMhyAXb6W7Qs8FgXwDro5oo8JRkkgzw+xaK8LJOgraTT8yeTvj5j0CUS0Q7kEUgCbGuH22fJJTeDYFdCoptwWpXWJgdC5Amtd1Tja7I9Gnoi0+k+SEbCVP/vnRa/3PX0orzWKQC8amohUHk2bwYJ0Rr42vBN73+CHPm1SgRleXPR9njkmQD36XNaEgxSZKciCnmmOvVRkeJahG0NRB6aYh+Hr+lPWXq/emgfuy0xMjgQwAAAZQAAAAMcnNhLXNoYTItNTEyAAABgBBXZlg/PibjGL6ylkT/Sxj7+OWHCzOK9COEjtA2VuJ2Q33IviOctMnY1wX1mh2lYQCCgN5tJBbltPVcO5bhYC4nabiAEdRQBKg0QT5YHoZ2kEk6l56ibvP1JAL4NHmueSsL+FaorEJH/sl63gI8loaCIVSIXyvR0tuIfdfe3wpu9m6eygDQ26Q03pvgbncItUmfxCwSNpxqgLatDFaaXMInS3k8X5KP0anvApQmLcsg2wMdWbXxy4wnvVXk1BLfkV3h5EskTM+DL++JJ485Lj0OD2OuxztBg7lw2PV7XgYz7GX0+KQ1ux5HDvHX1llcWVvWsI1aI35c75KylBaUTRnFSNfEAUDMoLqL2V64OD8peLuOfr1SFOFnS811Q48Or6nVkb2MimVkpPfamBXGDhUkT42IizFjA77AvXviovxyBUx+odzNKsnfWdm+V/D8TrLXsZOrQOme1P5WGrf94m6lXsAi8OgDNIhuHXKkyvloZNmwYeYT3h5pfQV68I/DdQ== john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_ecdsa_nistp521-cert.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp521-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHA1MjEtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgGK5Sm9O3GGXTro19mMNJevKtx0IiDF05zX6MGyuWBNkAAAAIbmlzdHA1MjEAAACFBAF2dKe+bG2Gl0Bhpher3OxjruX5Gdycx8CqfFyZTOcQgbO0VPOq027mZ3DlxRzvdU8DBidCFg9a7vlwqVgtSDF7zAE2GTg4YfAT9EGMrzrbIihcmNM0RB5BKzi96WReUqM/NzupVKJhBd0vMSJP7s73LiYwOIkTXL0Xxbc4u7G40MkfCgAAAAAAAAAAAAAAAQAAAAhqb2huLmRvZQAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAPS5OLqkfV6Bq855pdUGh55LqWPaGTETLCylbbsqacC8EF6KLbWU5QvNZH3SBC4PdZET71x91/kcGncKKbZ4+UwACuk2ZlebhLjJrZVmq6YHkk2QrhLIQYpIpDt0tqXZ1k8k19Y6ssSbY21V+ekNLmJPG9pEaxevfDR6CLCVZ8Z9j1x+J6A9lJOBpSM9U77sqGcXyn6bxU6BQlBmSV+AuPPF6zCvKFE2EVL0tQmkRkWSq2D6mEWRWYNfZnPFDg8YKMeiWIi8GkeMgsZ7p/xA/yMhyAXb6W7Qs8FgXwDro5oo8JRkkgzw+xaK8LJOgraTT8yeTvj5j0CUS0Q7kEUgCbGuH22fJJTeDYFdCoptwWpXWJgdC5Amtd1Tja7I9Gnoi0+k+SEbCVP/vnRa/3PX0orzWKQC8amohUHk2bwYJ0Rr42vBN73+CHPm1SgRleXPR9njkmQD36XNaEgxSZKciCnmmOvVRkeJahG0NRB6aYh+Hr+lPWXq/emgfuy0xMjgQwAAAZQAAAAMcnNhLXNoYTItNTEyAAABgNG/tgQoJKpwUZil+p9/jiAacq/8Kpw8c3uQYTKeZyzrDjpv5dNa0ASFqosZ3zHnD+A8bs6740nwds5Azeg2qFt6pYB0LdewOQbC6wXHZksHhZxgcHy5wx81QXI5xcyZpC3lY55KCmTcBu3y8CJEjqXBdWWyim4VA+41jkri493mKSmKeaD1fEctrFYB/2ahx+xwiNtlwHaqu1BBq5uaH+ndwNIEuuXGHi+JgXqCjIbvctW/B6PioKkZGzSG3xUYPBahlcKxbsmUrg5xNfbQDIq6vXXwka6/jP/Wi0k2WLrjXOqxjUgxsHqB6TBb6u+zU/sFuUQMZh7ZJTKTxvU4uv6MEXFMIze3G8lQW8uJ8+Rz3O8B9h6pxxYPHzWpu7wuxqefibIYfHY5weVsj7owedZg56x2hC2RC4RLEYioeWLuj9gEqnb4eQik4x8fVzrGX5+W73GHQScZZLHfgu9EsfLyUvcoPWX528pSLFxCEwHT1ZXnUNVczh4suemrtp3u1Q== john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_rsa_3072-cert.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg1Yz02d/yK4OFMeh2ieFBy5nhRubSM9kVGabUPFLQTK8AAAADAQABAAABgQCsngzCcay+lQ+34qUeUSH2m1ZYW9B0a2rxpMmvYFcOyL/hRPJwv8XO89T0+HQIZRC+xlM3BSqdFGs+B58MYXPvo3H+p00CJN8tUjvC3VD74kiXSNxIyhBpKCY1s58RxnWS/6bPQIYfnCVBiQZnkNe1T3isxND1Y71TnbSz5QN2xBkAtiGPH0dPM89yWbZpTjTCaIOfyZn2fBBsmp0zUgEJ7o9W9Lrxs1f0Pn+bZ4PqFSEUzlub7mAQ+RpwgGeLeWIFz+o6KQJPFiuRgzQU6ZsY+wjorVefzgeqpRiWGw/bEyUDck09B4B0IWoTtIiKRzd635nOo7Lz/1XgaMZ60WZD9T/labEWcKmtp4Y7NoCkep0DyYyoAgWrco4FD1r0g4WcVbsJQt8HzRy9UaHlh6YPY/xkk0bSiljpygEiT48FxniqE+6HY+7SbC1wz5QThY+UsIiDgFcg3BljskfT8Il3hateXI2wEXqww4+a+DxcHzypclYorbQKUzdzNLZRBNkAAAAAAAAAAAAAAAEAAAAIam9obi5kb2UAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQD0uTi6pH1egavOeaXVBoeeS6lj2hkxEywspW27KmnAvBBeii21lOULzWR90gQuD3WRE+9cfdf5HBp3Cim2ePlMAArpNmZXm4S4ya2VZqumB5JNkK4SyEGKSKQ7dLal2dZPJNfWOrLEm2NtVfnpDS5iTxvaRGsXr3w0egiwlWfGfY9cfiegPZSTgaUjPVO+7KhnF8p+m8VOgUJQZklfgLjzxeswryhRNhFS9LUJpEZFkqtg+phFkVmDX2ZzxQ4PGCjHoliIvBpHjILGe6f8QP8jIcgF2+lu0LPBYF8A66OaKPCUZJIM8PsWivCyToK2k0/Mnk74+Y9AlEtEO5BFIAmxrh9tnySU3g2BXQqKbcFqV1iYHQuQJrXdU42uyPRp6ItPpPkhGwlT/750Wv9z19KK81ikAvGpqIVB5Nm8GCdEa+NrwTe9/ghz5tUoEZXlz0fZ45JkA9+lzWhIMUmSnIgp5pjr1UZHiWoRtDUQemmIfh6/pT1l6v3poH7stMTI4EMAAAGUAAAADHJzYS1zaGEyLTUxMgAAAYACiJ2qVIszuCDzeDAsWdatWLDPgFKtqtBBBaZ5yP+xuN3xyih0wOzw16WRZNCo9rPae8KTWftV1KynFgM6yKt3Eh4nt5QFie4rrjaGB0OoTaKf/V0FOaudIWoh/4Adsu+6H1nyfvEUa6okOOAeA7A5+dzdD8eSYtXZn0MbUKH8bwKQqvaqxpAIIiGrMKcDIiTlWmGRGXuTFQ+3ejJEMOKr/ZGqyu7YgbtvsuAaV+hnnD5HBxD0tUvgKU4OwRXbo0QaAnXXyKL2rIQhvAlykurhwvIylohN9dYZTXQaFfpKcvSmXMV6ve/kxCmLP5l52L12MJ+dGidIgtDWyCAyuYV2y7z1iuNy99ltAj6S/tuQ9qdR8z0EPjEdarbVcmYFmrBJ8jF35UjzyiGmtpYImuFN4zlq54ggC/2SOJe4jIWqR5NZuBj2MovFJNrhWwPocm7SAcQI7G1OoTSvw7aUWd8eppfDJR98LmfsAK5n6Dq8cOjKaOmbG0YwPaSvFdSdOiU= john.doe@localhost 2 | -------------------------------------------------------------------------------- /t/test-keys/id_dsa-cert.pub: -------------------------------------------------------------------------------- 1 | ssh-dss-cert-v01@openssh.com AAAAHHNzaC1kc3MtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgF1+0+GizX6dIETlBAXDrxs581vVFIaARYYt+E6gAGu4AAACBAMV0EY/L9PVk7aHVxW8iZ8WLUYeSjCp/cjIBazlblLRrxP9g0OJ3TRv0ER4gUWXkSU9TVMzxeeEhQMNMlztVf3flyNbar1fzOguim+93CDJkITt9kxF68XIF7dj/1T71hKEC9F5ylE0q0WekWgXAbLaNdtbRFJYIV45mzk5Mjvc7AAAAFQDKGkxsB8cBOCmcx7IysEvO+VlBOQAAAIEApSV9OsCmjbi9PL/ve0ztZbm1JPFmE77TCPh7KJYEyo/8+HG1L72STiBkaUKAbptdxxahiqXkcBpvWl2H331vRGJEJ9/Fcsg93QILimbKGM/VXTw6DJq7mwFTQUVSeFSLnQPraVPyICnnO76sPOizJMexOwmrTKCN0ipgOQYBQtcAAACBAKc7EYtdHPZcfjTT4k/4OcTcPezpoi5xYxafeyKwDTxz52uV+wha9RzjDmg5rZLmPHAAVaC8ln+rGdcZnFvWw9mFthBL9bsaEJWfPoRdntHbhuiYAsZx6cT+Xs5n+xLrGbexlEln1rNwbjYUWPW4Vu3+v3t7Ol8nGk18Ro9ILGlLAAAAAAAAAAAAAAABAAAACGpvaG4uZG9lAAAAAAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAABlwAAAAdzc2gtcnNhAAAAAwEAAQAAAYEA9Lk4uqR9XoGrznml1QaHnkupY9oZMRMsLKVtuyppwLwQXoottZTlC81kfdIELg91kRPvXH3X+Rwadwoptnj5TAAK6TZmV5uEuMmtlWarpgeSTZCuEshBikikO3S2pdnWTyTX1jqyxJtjbVX56Q0uYk8b2kRrF698NHoIsJVnxn2PXH4noD2Uk4GlIz1TvuyoZxfKfpvFToFCUGZJX4C488XrMK8oUTYRUvS1CaRGRZKrYPqYRZFZg19mc8UODxgox6JYiLwaR4yCxnun/ED/IyHIBdvpbtCzwWBfAOujmijwlGSSDPD7Forwsk6CtpNPzJ5O+PmPQJRLRDuQRSAJsa4fbZ8klN4NgV0Kim3BaldYmB0LkCa13VONrsj0aeiLT6T5IRsJU/++dFr/c9fSivNYpALxqaiFQeTZvBgnRGvja8E3vf4Ic+bVKBGV5c9H2eOSZAPfpc1oSDFJkpyIKeaY69VGR4lqEbQ1EHppiH4ev6U9Zer96aB+7LTEyOBDAAABlAAAAAxyc2Etc2hhMi01MTIAAAGAHx0s0ZgqNxFXg+QCVS9jLVhvrgL55DZC9nlA03YzgE0jpI6Xr5dkYObbMHPaoP3Dh1QBNya42S0MWkRA+eVM3N/m89c8iJ/0JvcEnqIlJJSsPw468MLln90xpq3JY3uPg+5UEHQXKb3lboPUb3FqLmwEmotKfKsiGoiHfsdhxEimO3raxDwD/Zdpjc06mwkRlF/biVDeUZGvoPYi2BLcbEr59RGH6TlMKhS0s3AI8H2VHEHUBSIHcoLXhzhvhCSbre43PmvidSvmbTl8vb0QeJ+TpLhsffJXhwlorQ8oXgzz6hjPK3d/Y3WrflfOuluCVW4O2YH3xRU69tWcglbkq+Nn6tFSzG5gPe1PB/6iKJpZghNAbmIH+obnv567ixOv2xfBIRjB3H4xM8ZWNJQYyJwyYRyBGpBVmmxic+fRFNPrkgUGIcj3Nrjm6sC+B4vFNRWOXR42Rj2I5LTMFLkRXHSfnBRmQIR0nJbz+Il6gYERiRWAarAVFr1pIxzLzqTN john.doe@localhost 2 | -------------------------------------------------------------------------------- /cl-ssh-keys.asd: -------------------------------------------------------------------------------- 1 | (defpackage :cl-ssh-keys-system 2 | (:use :cl :asdf)) 3 | (in-package :cl-ssh-keys-system) 4 | 5 | (defsystem "cl-ssh-keys" 6 | :name "cl-ssh-keys" 7 | :long-name "cl-ssh-keys" 8 | :description "Common Lisp system for generating and parsing of OpenSSH keys" 9 | :version "0.7.0" 10 | :author "Marin Atanasov Nikolov " 11 | :maintainer "Marin Atanasov Nikolov " 12 | :license "BSD 2-Clause" 13 | :long-description #.(uiop:read-file-string 14 | (uiop:subpathname *load-pathname* "README.md")) 15 | :homepage "https://github.com/dnaeon/cl-ssh-keys" 16 | :bug-tracker "https://github.com/dnaeon/cl-ssh-keys" 17 | :source-control "https://github.com/dnaeon/cl-ssh-keys" 18 | :depends-on (:cl-rfc4251 19 | :ironclad 20 | :uiop 21 | :alexandria 22 | :cl-base64) 23 | :components ((:module "core" 24 | :pathname #P"src/" 25 | :components ((:file "package") 26 | (:file "base" :depends-on ("package")) 27 | (:file "rfc8017" :depends-on ("package")) 28 | (:file "generics" :depends-on ("package")) 29 | (:file "public-key" :depends-on ("package")) 30 | (:file "private-key" :depends-on ("package" "ciphers")) 31 | (:file "conditions" :depends-on ("package")) 32 | (:file "key-types" :depends-on ("package")) 33 | (:file "signature" :depends-on ("package")) 34 | (:file "ciphers" :depends-on ("package")))) 35 | (:module "keys" 36 | :pathname #P"src/" 37 | :depends-on ("core") 38 | :components ((:file "rsa") 39 | (:file "dsa") 40 | (:file "ed25519") 41 | (:file "ecdsa-nistp256") 42 | (:file "ecdsa-nistp384") 43 | (:file "ecdsa-nistp521") 44 | (:file "cert-key")))) 45 | :in-order-to ((test-op (test-op "cl-ssh-keys.test")))) 46 | -------------------------------------------------------------------------------- /src/generics.lisp: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2020 Marin Atanasov Nikolov 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer 10 | ;; in this position and unchanged. 11 | ;; 2. Redistributions in binary form must reproduce the above copyright 12 | ;; notice, this list of conditions and the following disclaimer in the 13 | ;; documentation and/or other materials provided with the distribution. 14 | ;; 15 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | ;; IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | (in-package :cl-ssh-keys) 27 | 28 | (defgeneric fingerprint (hash-spec key &key) 29 | (:documentation "Computes the fingerprint of the given KEY using the HASH-SPEC")) 30 | 31 | (defgeneric key-bits (key) 32 | (:documentation "Returns the number of bits for the key")) 33 | 34 | (defgeneric write-key (key &optional stream) 35 | (:documentation "Writes the key to the given stream in its text representation")) 36 | 37 | (defgeneric generate-key-pair (kind &key) 38 | (:documentation "Generates a new pair of public and private keys")) 39 | 40 | (defgeneric verify-signature (key message signature &key) 41 | (:documentation "Verifies the signature of the given message using the public key")) 42 | 43 | (defgeneric get-bytes-for-signing (key &key) 44 | (:documentation "Returns the bytes of the key which will be signed")) 45 | -------------------------------------------------------------------------------- /src/base.lisp: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2020-2021 Marin Atanasov Nikolov 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer 10 | ;; in this position and unchanged. 11 | ;; 2. Redistributions in binary form must reproduce the above copyright 12 | ;; notice, this list of conditions and the following disclaimer in the 13 | ;; documentation and/or other materials provided with the distribution. 14 | ;; 15 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | ;; IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | (in-package :cl-ssh-keys) 27 | 28 | (defclass base-key () 29 | ((kind 30 | :initarg :kind 31 | :initform (error "Must specify key kind") 32 | :reader key-kind 33 | :documentation "SSH key kind") 34 | (comment 35 | :initarg :comment 36 | :initform nil 37 | :accessor key-comment 38 | :documentation "Comment associated with the key")) 39 | (:documentation "Base class for representing an OpenSSH key")) 40 | 41 | (defclass base-ecdsa-nistp-key (base-key) 42 | ((identifier 43 | :initarg :identifier 44 | :initform (error "Must specify curve identifier") 45 | :reader ecdsa-curve-identifier 46 | :documentation "Identifier of the elliptic curve domain parameters")) 47 | (:documentation "Base class for representing an OpenSSH ECDSA key")) 48 | 49 | (defun write-key-to-path (key path) 50 | "Writes the given KEY to the destination PATH" 51 | (with-open-file (out path :direction :output) 52 | (write-key key out))) 53 | -------------------------------------------------------------------------------- /t/test-keys/id_rsa_3072: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | 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 38 | -----END OPENSSH PRIVATE KEY----- 39 | -------------------------------------------------------------------------------- /t/test-keys/id_rsa_unknown_key_type: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | 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 38 | -----END OPENSSH PRIVATE KEY----- 39 | -------------------------------------------------------------------------------- /t/test-keys/id_rsa_3072_host_ca: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | 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 38 | -----END OPENSSH PRIVATE KEY----- 39 | -------------------------------------------------------------------------------- /t/test-keys/id_rsa_3072_user_ca: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | 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 38 | -----END OPENSSH PRIVATE KEY----- 39 | -------------------------------------------------------------------------------- /t/test-keys/id_rsa_3072_encrypted_aes128-cbc: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | 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 38 | uFpweWY7mLfAPFwpK6cCuMCYbck= 39 | -----END OPENSSH PRIVATE KEY----- 40 | -------------------------------------------------------------------------------- /src/conditions.lisp: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2020 Marin Atanasov Nikolov 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer 10 | ;; in this position and unchanged. 11 | ;; 2. Redistributions in binary form must reproduce the above copyright 12 | ;; notice, this list of conditions and the following disclaimer in the 13 | ;; documentation and/or other materials provided with the distribution. 14 | ;; 15 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | ;; IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | (in-package :cl-ssh-keys) 27 | 28 | (define-condition base-error (simple-error) 29 | ((description 30 | :initarg :description 31 | :reader error-description)) 32 | (:documentation "Base error condition") 33 | (:report (lambda (condition stream) 34 | (format stream "~a" (error-description condition))))) 35 | 36 | (define-condition invalid-key-error (base-error) 37 | () 38 | (:documentation "Signaled when a key is detected as invalid") 39 | (:report (lambda (condition stream) 40 | (format stream "Invalid key error: ~a~&" (error-description condition))))) 41 | 42 | (define-condition key-type-mismatch-error (base-error) 43 | ((expected 44 | :initarg :expected 45 | :reader error-expected-key-type) 46 | (found 47 | :initarg :found 48 | :reader error-found-key-type)) 49 | (:documentation "Signaled when there is a mismatch between the known key type and the encoded key type") 50 | (:report (lambda (condition stream) 51 | (format stream "~a. Expected key type ~a, but found ~a~&" 52 | (error-description condition) 53 | (error-expected-key-type condition) 54 | (error-found-key-type condition))))) 55 | 56 | (define-condition unsupported-key-error (base-error) 57 | () 58 | (:documentation "Signaled when attempting to perform an operation on keys that are not supported") 59 | (:report (lambda (condition stream) 60 | (format stream "~a~&" (error-description condition))))) 61 | -------------------------------------------------------------------------------- /t/test-keys/id_rsa_invalid_padding: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | 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 52 | -----END OPENSSH PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /src/signature.lisp: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2020-2021 Marin Atanasov Nikolov 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer 10 | ;; in this position and unchanged. 11 | ;; 2. Redistributions in binary form must reproduce the above copyright 12 | ;; notice, this list of conditions and the following disclaimer in the 13 | ;; documentation and/or other materials provided with the distribution. 14 | ;; 15 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | ;; IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | (in-package :cl-ssh-keys) 27 | 28 | (defparameter *signature-types* 29 | '((:name "ssh-rsa" 30 | :digest :sha1) 31 | (:name "rsa-sha2-256" 32 | :digest :sha256) 33 | (:name "rsa-sha2-512" 34 | :digest :sha512) 35 | (:name "ssh-dss" 36 | :digest :sha1) 37 | (:name "ssh-ed25519" 38 | :digest :sha512) 39 | (:name "ecdsa-sha2-nistp256" 40 | :digest :sha256) 41 | (:name "ecdsa-sha2-nistp384" 42 | :digest :sha384) 43 | (:name "ecdsa-sha2-nistp521" 44 | :digest :sha512)) 45 | "OpenSSH certificate signature types") 46 | 47 | (defun get-signature-type (value) 48 | "Get the signature type with name identified by VALUE" 49 | (find value *signature-types* 50 | :key (lambda (item) 51 | (getf item :name)) 52 | :test #'equal)) 53 | 54 | (defun get-signature-type-or-lose (value) 55 | (let ((signature-type (get-signature-type value))) 56 | (unless signature-type 57 | (error 'base-error 58 | :description (format nil "Unknown signature type ~a" value))) 59 | signature-type)) 60 | 61 | (defclass signature () 62 | ((type 63 | :initarg :type 64 | :reader signature-type 65 | :initform (error "Must specify signature type") 66 | :documentation "Signature type") 67 | (blob 68 | :initarg :blob 69 | :reader signature-blob 70 | :initform (error "Must specify signature blob") 71 | :documentation "Computed signature blob")) 72 | (:documentation "Certificate signature")) 73 | 74 | 75 | (defmethod rfc4251:decode ((type (eql :cert-signature)) stream &key) 76 | "Decode certificate key signature from the given binary stream" 77 | (let* ((type-data (multiple-value-list (rfc4251:decode :string stream))) 78 | (blob-data (multiple-value-list (rfc4251:decode :buffer stream))) 79 | (type (first type-data)) 80 | (blob (first blob-data)) 81 | (total (+ (second type-data) (second blob-data))) 82 | (signature-type (get-signature-type-or-lose type)) 83 | (signature (make-instance 'signature 84 | :type signature-type 85 | :blob blob))) 86 | (values signature total))) 87 | 88 | (defmethod rfc4251:encode ((type (eql :cert-signature)) (value signature) stream &key) 89 | "Encode certificate signature into the given stream" 90 | (with-accessors ((type signature-type) (blob signature-blob)) value 91 | (let ((type-name (getf type :name))) 92 | (+ (rfc4251:encode :string type-name stream) 93 | (rfc4251:encode :buffer blob stream))))) 94 | -------------------------------------------------------------------------------- /src/key-types.lisp: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2020 Marin Atanasov Nikolov 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer 10 | ;; in this position and unchanged. 11 | ;; 2. Redistributions in binary form must reproduce the above copyright 12 | ;; notice, this list of conditions and the following disclaimer in the 13 | ;; documentation and/or other materials provided with the distribution. 14 | ;; 15 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | ;; IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | (in-package :cl-ssh-keys) 27 | 28 | (defparameter *key-types* 29 | '((:name "ssh-rsa" 30 | :plain-name "ssh-rsa" 31 | :short-name "RSA" 32 | :id :ssh-rsa 33 | :is-cert nil) 34 | (:name "ssh-rsa-cert-v01@openssh.com" 35 | :plain-name "ssh-rsa" 36 | :short-name "RSA-CERT" 37 | :id :ssh-rsa-cert-v01 38 | :is-cert t) 39 | (:name "ssh-dss" 40 | :plain-name "ssh-dss" 41 | :short-name "DSA" 42 | :id :ssh-dss 43 | :is-cert nil) 44 | (:name "ssh-dss-cert-v01@openssh.com" 45 | :plain-name "ssh-dss" 46 | :short-name "DSA-CERT" 47 | :id :ssh-dss-cert-v01 48 | :is-cert t) 49 | (:name "ecdsa-sha2-nistp256" 50 | :plain-name "ecdsa-sha2-nistp256" 51 | :short-name "ECDSA" 52 | :id :ecdsa-sha2-nistp256 53 | :is-cert nil) 54 | (:name "ecdsa-sha2-nistp384" 55 | :plain-name "ecdsa-sha2-nistp384" 56 | :short-name "ECDSA" 57 | :id :ecdsa-sha2-nistp384 58 | :is-cert nil) 59 | (:name "ecdsa-sha2-nistp521" 60 | :plain-name "ecdsa-sha2-nistp521" 61 | :short-name "ECDSA" 62 | :id :ecdsa-sha2-nistp521 63 | :is-cert nil) 64 | (:name "ecdsa-sha2-nistp256-cert-v01@openssh.com" 65 | :plain-name "ecdsa-sha2-nistp256" 66 | :short-name "ECDSA-CERT" 67 | :id :ecdsa-sha2-nistp256-cert-v01 68 | :is-cert t) 69 | (:name "ecdsa-sha2-nistp384-cert-v01@openssh.com" 70 | :plain-name "ecdsa-sha2-nistp384" 71 | :short-name "ECDSA-CERT" 72 | :id :ecdsa-sha2-nistp384-cert-v01 73 | :is-cert t) 74 | (:name "ecdsa-sha2-nistp521-cert-v01@openssh.com" 75 | :plain-name "ecdsa-sha2-nistp521" 76 | :short-name "ECDSA-CERT" 77 | :id :ecdsa-sha2-nistp521-cert-v01 78 | :is-cert t) 79 | (:name "ssh-ed25519" 80 | :plain-name "ssh-ed25519" 81 | :short-name "ED25519" 82 | :id :ssh-ed25519 83 | :is-cert nil) 84 | (:name "ssh-ed25519-cert-v01@openssh.com" 85 | :plain-name "ssh-ed25519" 86 | :short-name "ED25519-CERT" 87 | :id :ssh-ed25519-cert-v01 88 | :is-cert t)) 89 | "OpenSSH key types") 90 | 91 | (defun get-key-type (value &key (by :name)) 92 | "Get the key type identified by the given value and property" 93 | (find value *key-types* 94 | :key (lambda (item) 95 | (getf item by)) 96 | :test #'equal)) 97 | 98 | (defun get-key-type-or-lose (value &key (by :name)) 99 | (let ((key-type (get-key-type value :by by))) 100 | (unless key-type 101 | (error 'base-error 102 | :description (format nil "Unknown key type ~a" value))) 103 | key-type)) 104 | -------------------------------------------------------------------------------- /src/ciphers.lisp: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2020 Marin Atanasov Nikolov 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer 10 | ;; in this position and unchanged. 11 | ;; 2. Redistributions in binary form must reproduce the above copyright 12 | ;; notice, this list of conditions and the following disclaimer in the 13 | ;; documentation and/or other materials provided with the distribution. 14 | ;; 15 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | ;; IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | (in-package :cl-ssh-keys) 27 | 28 | (defparameter *default-cipher-name* 29 | "aes256-ctr" 30 | "Default cipher to use when encrypting a private key") 31 | 32 | (defparameter *ciphers* 33 | '((:name "3des-cbc" 34 | :blocksize 8 35 | :iv-length 8 36 | :key-length 24 37 | :mode :cbc 38 | :ironclad-cipher :3des) 39 | (:name "aes128-cbc" 40 | :blocksize 16 41 | :iv-length 16 42 | :key-length 16 43 | :mode :cbc 44 | :ironclad-cipher :aes) 45 | (:name "aes192-cbc" 46 | :blocksize 16 47 | :iv-length 16 48 | :key-length 24 49 | :mode :cbc 50 | :ironclad-cipher :aes) 51 | (:name "aes256-cbc" 52 | :blocksize 16 53 | :iv-length 16 54 | :key-length 32 55 | :mode :cbc 56 | :ironclad-cipher :aes) 57 | (:name "aes128-ctr" 58 | :blocksize 16 59 | :iv-length 16 60 | :key-length 16 61 | :mode :ctr 62 | :ironclad-cipher :aes) 63 | (:name "aes192-ctr" 64 | :blocksize 16 65 | :iv-length 16 66 | :key-length 24 67 | :mode :ctr 68 | :ironclad-cipher :aes) 69 | (:name "aes256-ctr" 70 | :blocksize 16 71 | :iv-length 16 72 | :key-length 32 73 | :mode :ctr 74 | :ironclad-cipher :aes) 75 | (:name "none" 76 | :blocksize 8 77 | :iv-length 0 78 | :key-length 0 79 | :mode nil 80 | :ironclad-cipher nil)) 81 | "Various ciphers used by OpenSSH that are supported") 82 | 83 | (defun get-cipher-by-name (name) 84 | "Get a cipher by its name" 85 | (find name *ciphers* 86 | :key (lambda (item) 87 | (getf item :name)) 88 | :test #'equal)) 89 | 90 | (defun get-cipher-by-name-or-lose (name) 91 | (let ((cipher-info (get-cipher-by-name name))) 92 | (unless cipher-info 93 | (error 'base-error 94 | :description (format nil "Unknown cipher name ~a" name))) 95 | cipher-info)) 96 | 97 | (defun get-all-cipher-names () 98 | "Returns a list of all supported cipher names" 99 | (mapcar (lambda (item) (getf item :name)) *ciphers*)) 100 | 101 | (defun get-cipher-for-encryption/decryption (cipher-name passphrase salt rounds) 102 | "Returns a cipher that can be used for encryption/decryption of a private key" 103 | (declare (type (simple-array (unsigned-byte 8) (*)) 104 | passphrase salt) 105 | (type string cipher-name) 106 | (type fixnum rounds)) 107 | (let* ((cipher-info (get-cipher-by-name-or-lose cipher-name)) 108 | (iv-length (getf cipher-info :iv-length)) 109 | (key-length (getf cipher-info :key-length)) 110 | (mode (getf cipher-info :mode)) 111 | (ironclad-cipher (getf cipher-info :ironclad-cipher)) 112 | (kdf (ironclad:make-kdf :bcrypt-pbkdf)) 113 | (key-and-iv (ironclad:derive-key kdf 114 | passphrase 115 | salt 116 | rounds 117 | (+ key-length iv-length))) 118 | (key (subseq key-and-iv 0 key-length)) 119 | (iv (subseq key-and-iv key-length (+ key-length iv-length)))) 120 | (ironclad:make-cipher ironclad-cipher 121 | :key key 122 | :mode mode 123 | :initialization-vector iv))) 124 | -------------------------------------------------------------------------------- /src/rfc8017.lisp: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2020-2021 Marin Atanasov Nikolov 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer 10 | ;; in this position and unchanged. 11 | ;; 2. Redistributions in binary form must reproduce the above copyright 12 | ;; notice, this list of conditions and the following disclaimer in the 13 | ;; documentation and/or other materials provided with the distribution. 14 | ;; 15 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | ;; IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | (in-package :cl-ssh-keys) 27 | 28 | (defparameter *emsa-pkcs1-v1_5-digest-info* 29 | '(:md2 #(#x30 #x20 #x30 #x0c #x06 #x08 #x2a #x86 #x48 #x86 #xf7 #x0d #x02 #x02 #x05 #x00 #x04 #x10) 30 | :md5 #(#x30 #x20 #x30 #x0c #x06 #x08 #x2a #x86 #x48 #x86 #xf7 #x0d #x02 #x05 #x05 #x00 #x04 #x10) 31 | :sha1 #(#x30 #x21 #x30 #x09 #x06 #x05 #x2b #x0e #x03 #x02 #x1a #x05 #x00 #x04 #x14) 32 | :sha256 #(#x30 #x31 #x30 #x0d #x06 #x09 #x60 #x86 #x48 #x01 #x65 #x03 #x04 #x02 #x01 #x05 #x00 #x04 #x20) 33 | :sha384 #(#x30 #x41 #x30 #x0d #x06 #x09 #x60 #x86 #x48 #x01 #x65 #x03 #x04 #x02 #x02 #x05 #x00 #x04 #x30) 34 | :sha512 #(#x30 #x51 #x30 #x0d #x06 #x09 #x60 #x86 #x48 #x01 #x65 #x03 #x04 #x02 #x03 #x05 #x00 #x04 #x40)) 35 | "DigestInfo DER encoding of the known hash functions. See RFC 8017, section 9.2, notes 1.") 36 | 37 | (defun i2osp (n &key n-bits) 38 | "Integer-to-Octet-String primitive. See RFC 8017, section 4.1" 39 | (declare (type integer n)) 40 | (let ((n-bits (or n-bits (integer-length n)))) 41 | (ironclad:integer-to-octets n :n-bits n-bits))) 42 | 43 | (defun os2ip (octet-vec) 44 | "Octet-String-to-Integer primitive. See RFC 8017, section 4.2" 45 | (ironclad:octets-to-integer octet-vec)) 46 | 47 | (defun rsasp1 (priv-key message) 48 | "RSA signature primitive. See RFC 8017, section 5.2.1" 49 | (declare (type integer message)) 50 | (let ((n (ironclad:rsa-key-modulus priv-key)) 51 | (d (ironclad:rsa-key-exponent priv-key))) 52 | (unless (<= 0 message (1- n)) 53 | (error 'invalid-signature-error :description "rsasp1: message representative out of range")) 54 | (ironclad:expt-mod message d n))) 55 | 56 | (defun rsavp1 (public-key signature) 57 | "RSA verification primitive. See RFC 8017, section 5.2.2" 58 | (declare (type integer signature)) 59 | (let ((n (ironclad:rsa-key-modulus public-key)) 60 | (e (ironclad:rsa-key-exponent public-key))) 61 | (unless (<= 0 signature (1- n)) 62 | (error 'invalid-signature-error :description "rsavp1: signature representative out of range")) 63 | (ironclad:expt-mod signature e n))) 64 | 65 | (defun emsa-pkcs1-v1_5-encode (digest-spec message em-len) 66 | "EMSA-PKCS1-v1_5 encoding method. See RFC 8017, section 9.2" 67 | (unless (member digest-spec *emsa-pkcs1-v1_5-digest-info*) 68 | (error 'invalid-signature-error 69 | :description "emsa-pkcs1-v1_5-encode: unsupported digest spec")) 70 | (let* ((algorithm-identifier (getf *emsa-pkcs1-v1_5-digest-info* digest-spec)) 71 | (h (ironclad:digest-sequence digest-spec message)) ;; Step 1 72 | (tt (concatenate '(vector (unsigned-byte 8) *) ;; Step 2 73 | algorithm-identifier 74 | h)) 75 | (tt-len (length tt))) 76 | (when (< em-len (+ tt-len 11)) ;; Step 3 77 | (error 'invalid-signature-error 78 | :description "emsa-pkcs1-v1_5-encode: intended encoded message length too short")) 79 | (let* ((ps (make-array (- em-len tt-len 3) ;; Step 4 80 | :element-type '(unsigned-byte 8) 81 | :initial-element #xff))) 82 | (when (< (length ps) 8) 83 | (error 'invalid-signature-error 84 | :description "emsa-pkcs1-v1_5-encode: PS length should be at least 8 octets")) 85 | ;; Step 5 and 6 86 | (concatenate '(vector (unsigned-byte 8) *) 87 | #(#x00 #x01) ps #(#x00) tt)))) 88 | 89 | (defun rsassa-pkcs1-v1_5-sign (priv-key message digest-spec) 90 | "RSASSA-PKCS1-v1_5 signature generation. See RFC 8017, section 8.2.1" 91 | (let* ((n (ironclad:rsa-key-modulus priv-key)) 92 | (k (ceiling (integer-length n) 8)) 93 | (em (emsa-pkcs1-v1_5-encode digest-spec message k)) ;; Step 1 94 | (m (os2ip em)) ;; Step 2a 95 | (s (rsasp1 priv-key m))) ;; Step 2b 96 | ;; Step 2c and 3 97 | (i2osp s :n-bits (* 8 k)))) 98 | 99 | (defun rsassa-pkcs1-v1_5-verify (public-key message signature digest-spec) 100 | "RSASSA-PKCS1-v1_5 signature verification. See RFC 8017, section 8.2.2" 101 | (let* ((n (ironclad:rsa-key-modulus public-key)) 102 | (k (ceiling (integer-length n) 8))) 103 | ;; Step 1 104 | (unless (= k (length signature)) 105 | (error 'invalid-signature-error 106 | :description "rsassa-pkcs1-v1_5-verify: invalid signature")) 107 | (let* ((s (os2ip signature)) ;; Step 2a 108 | (m (rsavp1 public-key s)) ;; Step 2b 109 | (em (i2osp m :n-bits (* 8 k))) ;; Step 2c 110 | (em-prime (emsa-pkcs1-v1_5-encode digest-spec message k))) ;; Step 3 111 | ;; Step 4 112 | (equalp em em-prime)))) 113 | -------------------------------------------------------------------------------- /src/package.lisp: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2020 Marin Atanasov Nikolov 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer 10 | ;; in this position and unchanged. 11 | ;; 2. Redistributions in binary form must reproduce the above copyright 12 | ;; notice, this list of conditions and the following disclaimer in the 13 | ;; documentation and/or other materials provided with the distribution. 14 | ;; 15 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | ;; IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | (in-package :cl-user) 27 | (defpackage :cl-ssh-keys 28 | (:use :cl) 29 | (:nicknames :ssh-keys) 30 | (:import-from 31 | :ironclad 32 | :rsa-key-exponent 33 | :rsa-key-modulus 34 | :rsa-key-prime-p 35 | :rsa-key-prime-q 36 | :dsa-key-p 37 | :dsa-key-q 38 | :dsa-key-g 39 | :dsa-key-y 40 | :dsa-key-x 41 | :ed25519-key-x 42 | :ed25519-key-y 43 | :secp256r1-key-y 44 | :secp256r1-key-x 45 | :secp384r1-key-y 46 | :secp384r1-key-x 47 | :secp521r1-key-y 48 | :secp521r1-key-x) 49 | (:import-from :cl-rfc4251) 50 | (:import-from :uiop) 51 | (:import-from :alexandria) 52 | (:import-from :cl-base64) 53 | (:export 54 | ;; base 55 | :base-key 56 | :key-kind 57 | :key-comment 58 | :base-ecdsa-nistp-key 59 | :ecdsa-curve-identifier 60 | :write-key-to-path 61 | 62 | ;; rfc8017 63 | :*emsa-pkcs1-v1_5-digest-info* 64 | :i2osp 65 | :os2ip 66 | :rsasp1 67 | :rsavp1 68 | :emsa-pkcs1-v1_5-encode 69 | :rsassa-pkcs1-v1_5-sign 70 | :rsassa-pkcs1-v1_5-verify 71 | 72 | ;; conditions 73 | :invalid-public-key-error 74 | :key-type-mismatch-error 75 | :unsupported-key-error 76 | :invalid-signature-error 77 | 78 | ;; generics 79 | :fingerprint 80 | :key-bits 81 | :write-key 82 | :generate-key-pair 83 | :verify-signature 84 | :get-bytes-for-signing 85 | 86 | ;; key-types 87 | :*key-types* 88 | :get-key-type 89 | :get-key-type-or-lose 90 | 91 | ;; ciphers 92 | :*ciphers* 93 | :*default-cipher-name* 94 | :get-cipher-by-name 95 | :get-cipher-by-name-or-lose 96 | :get-all-cipher-names 97 | 98 | ;; public-key 99 | :base-public-key 100 | :base-ecdsa-nistp-public-key 101 | :parse-public-key 102 | :parse-public-key-file 103 | :with-public-key 104 | :with-public-key-file 105 | 106 | ;; private-key 107 | :*default-kdf-rounds* 108 | :+kdf-salt-size+ 109 | :+private-key-auth-magic+ 110 | :+private-key-mark-begin+ 111 | :+private-key-mark-end+ 112 | :base-private-key 113 | :base-ecdsa-nistp-private-key 114 | :embedded-public-key 115 | :key-cipher-name 116 | :key-kdf-name 117 | :key-kdf-salt 118 | :key-kdf-rounds 119 | :key-passphrase 120 | :key-checksum-int 121 | :extract-private-key 122 | :extract-private-key-from-file 123 | :private-key-padding-is-correct-p 124 | :parse-private-key 125 | :parse-private-key-file 126 | :with-private-key 127 | :with-private-key-file 128 | 129 | ;; rsa 130 | :rsa-public-key 131 | :rsa-private-key 132 | :rsa-key-exponent ;; Re-export from ironclad 133 | :rsa-key-modulus ;; Re-export from ironclad 134 | :rsa-key-prime-p ;; Re-export from ironclad 135 | :rsa-key-prime-q ;; Re-export from ironclad 136 | 137 | ;; dsa 138 | :dsa-public-key 139 | :dsa-private-key 140 | :dsa-key-p ;; Re-export from ironclad 141 | :dsa-key-q ;; Re-export from ironclad 142 | :dsa-key-g ;; Re-export from ironclad 143 | :dsa-key-y ;; Re-export from ironclad 144 | :dsa-key-x ;; Re-export from ironclad 145 | 146 | ;; ed25519 147 | :ed25519-public-key 148 | :ed25519-private-key 149 | :ed25519-key-x ;; Re-export from ironclad 150 | :ed25519-key-y ;; Re-export from ironclad 151 | 152 | ;; ecdsa-nistp256 153 | :+nistp256-identifier+ 154 | :ecdsa-nistp256-public-key 155 | :ecdsa-nistp256-private-key 156 | :secp256r1-key-y ;; Re-export from ironclad 157 | :secp256r1-key-x ;; Re-export from ironclad 158 | 159 | ;; ecdsa-nistp384 160 | :+nistp384-identifier+ 161 | :ecdsa-nistp384-public-key 162 | :ecdsa-nistp384-private-key 163 | :secp384r1-key-y ;; Re-export from ironclad 164 | :secp384r1-key-x ;; Re-export from ironclad 165 | 166 | ;; ecdsa-nistp521 167 | :+nistp521-identifier+ 168 | :ecdsa-nistp521-public-key 169 | :ecdsa-nistp521-private-key 170 | :secp521r1-key-y ;; Re-export from ironclad 171 | :secp521r1-key-x ;; Re-export from ironclad 172 | 173 | ;; cert-key 174 | :+ssh-cert-type-user+ 175 | :+ssh-cert-type-host+ 176 | :+ssh-cert-max-valid-to+ 177 | :describe-cert-option 178 | :get-supported-cert-options 179 | :get-cert-critical-options 180 | :get-signature-type 181 | :get-signature-type-or-lose 182 | :signature 183 | :signature-type 184 | :signature-blob 185 | :certificate 186 | :cert-nonce 187 | :cert-key 188 | :cert-serial 189 | :cert-type 190 | :cert-key-id 191 | :cert-valid-principals 192 | :cert-valid-after 193 | :cert-valid-before 194 | :cert-critical-options 195 | :cert-extensions 196 | :cert-reserved 197 | :cert-signature-key 198 | :cert-signature)) 199 | (in-package :cl-ssh-keys) 200 | -------------------------------------------------------------------------------- /src/ed25519.lisp: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2020 Marin Atanasov Nikolov 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer 10 | ;; in this position and unchanged. 11 | ;; 2. Redistributions in binary form must reproduce the above copyright 12 | ;; notice, this list of conditions and the following disclaimer in the 13 | ;; documentation and/or other materials provided with the distribution. 14 | ;; 15 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | ;; IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | (in-package :cl-ssh-keys) 27 | 28 | (defconstant +ed25519-public-key-bytes+ 29 | 32 30 | "Number of bytes for an Ed25519 public key") 31 | 32 | (defconstant +ed25519-secret-key-bytes+ 33 | 64 34 | "Number of bytes for an Ed25519 secret key") 35 | 36 | (defclass ed25519-public-key (base-public-key ironclad:ed25519-public-key) 37 | () 38 | (:documentation "Represents an OpenSSH Ed25519 public key")) 39 | 40 | (defmethod verify-signature ((key ed25519-public-key) message (signature signature) &key) 41 | "Verifies the SIGNATURE of MESSAGE according to RFC 8032, section 5.1.7" 42 | (let ((blob (signature-blob signature))) 43 | (ironclad:verify-signature key message blob))) 44 | 45 | (defmethod rfc4251:decode ((type (eql :ed25519-public-key)) stream &key kind comment) 46 | "Decodes an Ed25519 public key from the given binary stream. 47 | See https://tools.ietf.org/html/draft-josefsson-eddsa-ed25519-03 for more details." 48 | (unless kind 49 | (error 'invalid-key-error 50 | :description "Public key kind was not specified")) 51 | (let* ((y-data (multiple-value-list (rfc4251:decode :buffer stream))) 52 | (y (first y-data)) ;; The public key 53 | (size (second y-data)) ;; Total number of bytes decoded 54 | (pk (make-instance 'ed25519-public-key 55 | :kind kind 56 | :comment comment 57 | :y y))) 58 | (values pk size))) 59 | 60 | (defmethod rfc4251:encode ((type (eql :ed25519-public-key)) (key ed25519-public-key) stream &key) 61 | "Encodes the Ed25519 public key into the given binary stream." 62 | (rfc4251:encode :buffer (ironclad:ed25519-key-y key) stream)) 63 | 64 | (defmethod key-bits ((key ed25519-public-key)) 65 | "Returns the number of bits for the Ed25519 public key" 66 | 256) 67 | 68 | (defclass ed25519-private-key (base-private-key ironclad:ed25519-private-key) 69 | () 70 | (:documentation "Represents an OpenSSH Ed25519 private key")) 71 | 72 | (defmethod rfc4251:decode ((type (eql :ed25519-private-key)) stream &key kind public-key 73 | cipher-name kdf-name 74 | kdf-salt kdf-rounds 75 | passphrase checksum-int) 76 | "Decodes an Ed25519 private key from the given stream" 77 | (let* ((y (rfc4251:decode :buffer stream)) ;; Public key buffer 78 | (secret-buffer (rfc4251:decode :buffer stream))) ;; Buffer which holds the private key + public key 79 | (unless (= (length y) +ed25519-public-key-bytes+) 80 | (error 'invalid-key-error 81 | :description "Invalid number of bytes for Ed25519 public key")) 82 | 83 | (unless (= (length secret-buffer) +ed25519-secret-key-bytes+) 84 | (error 'invalid-key-error 85 | :description "Invalid number of bytes for Ed25519 secret key")) 86 | 87 | ;; The public components must match 88 | ;; Verify that the public key we've just decoded matches with the one 89 | ;; that was provided to us. 90 | (unless (equalp y (ironclad:ed25519-key-y public-key)) 91 | (error 'invalid-key-error 92 | :description "Invalid Ed25519 key. Decoded and provided public keys mismatch")) 93 | 94 | ;; Verify that the public key contained within the secret buffer 95 | ;; matches with the one that was provided to us. 96 | ;; The subsequence 32..64 from the secret buffer holds the public key. 97 | (unless (equalp (ironclad:ed25519-key-y public-key) 98 | (subseq secret-buffer 32)) 99 | (error 'invalid-key-error 100 | :description "Invalid Ed25519 key. Decoded and provided public keys mismatch with secret buffer")) 101 | 102 | (make-instance 'ed25519-private-key 103 | :kind kind 104 | :public-key public-key 105 | :cipher-name cipher-name 106 | :kdf-name kdf-name 107 | :kdf-salt kdf-salt 108 | :kdf-rounds kdf-rounds 109 | :checksum-int checksum-int 110 | :passphrase passphrase 111 | :y y 112 | :x (subseq secret-buffer 0 32)))) ;; The private key is in the first 32 bytes of the secret buffer 113 | 114 | (defmethod rfc4251:encode ((type (eql :ed25519-private-key)) (key ed25519-private-key) stream &key) 115 | "Encodes the Ed25519 private key into the given binary stream" 116 | (let* ((y (ironclad:ed25519-key-y key)) ;; Public key 117 | (x (ironclad:ed25519-key-x key)) ;; Private key 118 | (secret-buffer (rfc4251:make-binary-output-stream))) ;; The secret buffer holds the private + public key 119 | (rfc4251:encode :raw-bytes x secret-buffer) 120 | (rfc4251:encode :raw-bytes y secret-buffer) 121 | (+ 122 | (rfc4251:encode :buffer y stream) 123 | (rfc4251:encode :buffer (rfc4251:get-binary-stream-bytes secret-buffer) stream)))) 124 | 125 | (defmethod key-bits ((key ed25519-private-key)) 126 | "Returns the number of bits of the embedded public key" 127 | (with-slots (public-key) key 128 | (key-bits public-key))) 129 | 130 | (defmethod generate-key-pair ((kind (eql :ed25519)) &key comment passphrase) 131 | "Generates a new pair of Ed25519 public and private keys" 132 | (let* ((cipher-name (if passphrase *default-cipher-name* "none")) 133 | (kdf-name (if passphrase "bcrypt" "none")) 134 | (key-type (get-key-type-or-lose :ssh-ed25519 :by :id)) 135 | (checksum-int (ironclad:random-bits 32)) 136 | (priv-pub-pair (multiple-value-list (ironclad:generate-key-pair :ed25519))) 137 | (ironclad-priv-key (first priv-pub-pair)) 138 | (ironclad-pub-key (second priv-pub-pair)) 139 | (pub-key (make-instance 'ed25519-public-key 140 | :kind key-type 141 | :comment comment 142 | :y (ironclad:ed25519-key-y ironclad-pub-key))) 143 | (priv-key (make-instance 'ed25519-private-key 144 | :public-key pub-key 145 | :cipher-name cipher-name 146 | :kdf-name kdf-name 147 | :passphrase passphrase 148 | :checksum-int checksum-int 149 | :kind key-type 150 | :comment comment 151 | :y (ironclad:ed25519-key-y ironclad-pub-key) 152 | :x (ironclad:ed25519-key-x ironclad-priv-key)))) 153 | (values priv-key pub-key))) 154 | -------------------------------------------------------------------------------- /src/dsa.lisp: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2020 Marin Atanasov Nikolov 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer 10 | ;; in this position and unchanged. 11 | ;; 2. Redistributions in binary form must reproduce the above copyright 12 | ;; notice, this list of conditions and the following disclaimer in the 13 | ;; documentation and/or other materials provided with the distribution. 14 | ;; 15 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | ;; IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | (in-package :cl-ssh-keys) 27 | 28 | (defclass dsa-public-key (base-public-key ironclad:dsa-public-key) 29 | () 30 | (:documentation "Represents an OpenSSH DSA public key")) 31 | 32 | (defmethod verify-signature ((key dsa-public-key) message (signature signature) &key) 33 | "Verifies the SIGNATURE of MESSAGE according to RFC 4253, section 6.6" 34 | (let ((digest-spec (getf (signature-type signature) :digest)) 35 | (blob (signature-blob signature))) 36 | (ironclad:verify-signature key (ironclad:digest-sequence digest-spec message) blob))) 37 | 38 | (defmethod rfc4251:decode ((type (eql :dsa-public-key)) stream &key kind comment) 39 | "Decodes a DSA public key from the given binary stream as defined in FIPS-186-2" 40 | (unless kind 41 | (error 'invalid-key-error 42 | :description "Public key kind was not specified")) 43 | ;; DSA parameters as defined in FIPS-186-2, section 4. 44 | (let* ((p-data (multiple-value-list (rfc4251:decode :mpint stream))) 45 | (q-data (multiple-value-list (rfc4251:decode :mpint stream))) 46 | (g-data (multiple-value-list (rfc4251:decode :mpint stream))) 47 | (y-data (multiple-value-list (rfc4251:decode :mpint stream))) 48 | (size (+ (second p-data) 49 | (second q-data) 50 | (second g-data) 51 | (second y-data))) ;; Total number of bytes read from the stream 52 | (p-q-g-group (list :p (first p-data) :q (first q-data) :g (first g-data))) 53 | (y (first y-data)) 54 | (group (apply #'make-instance 'ironclad::discrete-logarithm-group p-q-g-group)) 55 | (pk (make-instance 'dsa-public-key 56 | :kind kind 57 | :comment comment 58 | :group group 59 | :y y))) 60 | (values pk size))) 61 | 62 | (defmethod rfc4251:encode ((type (eql :dsa-public-key)) (key dsa-public-key) stream &key) 63 | "Encodes the DSA public key into the given binary stream." 64 | (destructuring-bind (&key p q g y) (ironclad:destructure-public-key key) 65 | (+ 66 | (rfc4251:encode :mpint p stream) 67 | (rfc4251:encode :mpint q stream) 68 | (rfc4251:encode :mpint g stream) 69 | (rfc4251:encode :mpint y stream)))) 70 | 71 | (defmethod key-bits ((key dsa-public-key)) 72 | "Returns the number of bits for the DSA public key" 73 | (integer-length (ironclad:dsa-key-p key))) 74 | 75 | (defclass dsa-private-key (base-private-key ironclad:dsa-private-key) 76 | () 77 | (:documentation "Represents an OpenSSH DSA private key")) 78 | 79 | (defmethod rfc4251:decode ((type (eql :dsa-private-key)) stream &key kind public-key 80 | cipher-name kdf-name 81 | kdf-salt kdf-rounds 82 | passphrase checksum-int) 83 | "Decodes a DSA private key from the given stream" 84 | ;; The DSA parameters as defined in FIPS-186-2 85 | (let* ((p (rfc4251:decode :mpint stream)) 86 | (q (rfc4251:decode :mpint stream)) 87 | (g (rfc4251:decode :mpint stream)) 88 | (y (rfc4251:decode :mpint stream)) 89 | (x (rfc4251:decode :mpint stream)) 90 | (pub-key-components (ironclad:destructure-public-key public-key))) 91 | ;; The public components encoded in the encrypted section 92 | ;; must match with the already decoded public key. 93 | (unless (and (= p (getf pub-key-components :p)) 94 | (= q (getf pub-key-components :q)) 95 | (= g (getf pub-key-components :g)) 96 | (= y (getf pub-key-components :y))) 97 | (error 'invalid-key-error 98 | :description "Invalid DSA key. Public keys mismatch")) 99 | 100 | (make-instance 'dsa-private-key 101 | :kind kind 102 | :public-key public-key 103 | :cipher-name cipher-name 104 | :kdf-name kdf-name 105 | :kdf-salt kdf-salt 106 | :kdf-rounds kdf-rounds 107 | :passphrase passphrase 108 | :checksum-int checksum-int 109 | :group (make-instance 'ironclad::discrete-logarithm-group :p p :q q :g g) 110 | :x x 111 | :y y))) 112 | 113 | (defmethod rfc4251:encode ((type (eql :dsa-private-key)) (key dsa-private-key) stream &key) 114 | "Encodes the DSA private key into the given binary stream" 115 | (destructuring-bind (&key p q g y x) (ironclad:destructure-private-key key) 116 | (+ 117 | (rfc4251:encode :mpint p stream) 118 | (rfc4251:encode :mpint q stream) 119 | (rfc4251:encode :mpint g stream) 120 | (rfc4251:encode :mpint y stream) 121 | (rfc4251:encode :mpint x stream)))) 122 | 123 | (defmethod key-bits ((key dsa-private-key)) 124 | "Returns the number of bits of the embedded public key" 125 | (with-slots (public-key) key 126 | (integer-length (ironclad:dsa-key-p public-key)))) 127 | 128 | (defmethod generate-key-pair ((kind (eql :dsa)) &key comment passphrase) 129 | "Generates a new pair of DSA public and private keys" 130 | (let* ((cipher-name (if passphrase *default-cipher-name* "none")) 131 | (kdf-name (if passphrase "bcrypt" "none")) 132 | (key-type (get-key-type-or-lose :ssh-dss :by :id)) 133 | (checksum-int (ironclad:random-bits 32)) 134 | (priv-pub-pair (multiple-value-list (ironclad:generate-key-pair :dsa :num-bits 1024))) ;; DSA keys must be exactly 1024 bits 135 | (ironclad-priv-key (first priv-pub-pair)) 136 | (ironclad-pub-key (second priv-pub-pair)) 137 | (pub-key (make-instance 'dsa-public-key 138 | :group (ironclad::group ironclad-pub-key) 139 | :y (ironclad:dsa-key-y ironclad-pub-key) 140 | :kind key-type 141 | :comment comment)) 142 | (priv-key (make-instance 'dsa-private-key 143 | :public-key pub-key 144 | :cipher-name cipher-name 145 | :kdf-name kdf-name 146 | :passphrase passphrase 147 | :checksum-int checksum-int 148 | :kind key-type 149 | :comment comment 150 | :group (ironclad::group ironclad-pub-key) 151 | :x (ironclad:dsa-key-x ironclad-priv-key) 152 | :y (ironclad:dsa-key-y ironclad-priv-key)))) 153 | (values priv-key pub-key))) 154 | -------------------------------------------------------------------------------- /src/ecdsa-nistp384.lisp: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2020 Marin Atanasov Nikolov 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer 10 | ;; in this position and unchanged. 11 | ;; 2. Redistributions in binary form must reproduce the above copyright 12 | ;; notice, this list of conditions and the following disclaimer in the 13 | ;; documentation and/or other materials provided with the distribution. 14 | ;; 15 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | ;; IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | (in-package :cl-ssh-keys) 27 | 28 | (alexandria:define-constant +nistp384-identifier+ 29 | "nistp384" 30 | :test #'equal 31 | :documentation "NIST name of the curve") 32 | 33 | (defclass ecdsa-nistp384-public-key (base-ecdsa-nistp-public-key ironclad:secp384r1-public-key) 34 | () 35 | (:documentation "Represents an OpenSSH ECDSA NIST P-384 public key")) 36 | 37 | (defmethod rfc4251:decode ((type (eql :ecdsa-nistp384-public-key)) stream &key kind comment) 38 | "Decodes an ECDSA NIST P-384 public key from the given binary stream" 39 | (unless kind 40 | (error 'invalid-key-error 41 | :description "Public key kind was not specified")) 42 | ;; The `[identifier]` and `Q` fields as defined in RFC 5656, section 3.1. 43 | ;; The `Q` field is the public key, which is represented as `y` in 44 | ;; ironclad:secp384r1 class. This is an `mpint` value. 45 | ;; See https://tools.ietf.org/search/rfc4492#appendix-A for the 46 | ;; various names under which NIST P-384 curves are known. 47 | (let* ((identifier-data (multiple-value-list (rfc4251:decode :string stream))) ;; Identifier of elliptic curve domain parameters 48 | (q-data (multiple-value-list (rfc4251:decode :buffer stream))) ;; Public key 49 | (size (+ (second identifier-data) (second q-data))) ;; Total number of bytes read from the stream 50 | (pk (make-instance 'ecdsa-nistp384-public-key 51 | :kind kind 52 | :comment comment 53 | :identifier (first identifier-data) 54 | :y (first q-data)))) 55 | (values pk size))) 56 | 57 | (defmethod rfc4251:encode ((type (eql :ecdsa-nistp384-public-key)) (key ecdsa-nistp384-public-key) stream &key) 58 | "Encodes the ECDSA NIST P-384 public key into the given binary stream." 59 | (with-accessors ((identifier ecdsa-curve-identifier) (y ironclad:secp384r1-key-y)) key 60 | (+ 61 | (rfc4251:encode :string identifier stream) 62 | (rfc4251:encode :buffer y stream)))) 63 | 64 | (defmethod key-bits ((key ecdsa-nistp384-public-key)) 65 | "Returns the number of bits for the ECDSA NIST P-384 public key" 66 | ironclad::+secp384r1-bits+) 67 | 68 | (defclass ecdsa-nistp384-private-key (base-ecdsa-nistp-private-key ironclad:secp384r1-private-key) 69 | () 70 | (:documentation "Represents an OpenSSH ECDSA NIST P-384 private key")) 71 | 72 | (defmethod rfc4251:decode ((type (eql :ecdsa-nistp384-private-key)) stream &key kind public-key 73 | cipher-name kdf-name 74 | kdf-salt kdf-rounds 75 | passphrase checksum-int) 76 | "Decodes a ECDSA NIST P-384 private key from the given stream" 77 | (let* (identifier ;; Curve identifier 78 | d ;; Private key 79 | q) ;; Public key 80 | ;; Decode curve identifier. The decoded identifier must match with the 81 | ;; curve identifier. 82 | (setf identifier (rfc4251:decode :string stream)) 83 | (unless (string= identifier +nistp384-identifier+) 84 | (error 'invalid-key-error 85 | :description "Invalid ECDSA NIST P-384 key. Curve identifiers mismatch")) 86 | 87 | ;; Public key, also embedded in the encrypted section. Must match with the 88 | ;; one of the provided public key. 89 | (setf q (rfc4251:decode :buffer stream)) 90 | (unless (equalp q (ironclad:secp384r1-key-y public-key)) 91 | (error 'invalid-key-error 92 | :description "Invalid ECDSA NIST P-384 key. Decoded and provided public keys mismatch")) 93 | 94 | ;; Decode private key 95 | (setf d (rfc4251:decode :buffer stream)) 96 | 97 | (make-instance 'ecdsa-nistp384-private-key 98 | :kind kind 99 | :public-key public-key 100 | :cipher-name cipher-name 101 | :kdf-name kdf-name 102 | :kdf-salt kdf-salt 103 | :kdf-rounds kdf-rounds 104 | :passphrase passphrase 105 | :checksum-int checksum-int 106 | :identifier identifier 107 | :y q 108 | :x d))) 109 | 110 | (defmethod rfc4251:encode ((type (eql :ecdsa-nistp384-private-key)) (key ecdsa-nistp384-private-key) stream &key) 111 | "Encodes the ECDSA NIST P-384 private key into the given binary stream" 112 | (let ((identifier (ecdsa-curve-identifier key)) 113 | (y (ironclad:secp384r1-key-y key)) 114 | (x (ironclad:secp384r1-key-x key))) 115 | (+ 116 | (rfc4251:encode :string identifier stream) ;; Curve identifier 117 | (rfc4251:encode :buffer y stream) ;; Public key 118 | (rfc4251:encode :buffer x stream)))) ;; Private key 119 | 120 | (defmethod key-bits ((key ecdsa-nistp384-private-key)) 121 | "Returns the number of bits of the embedded public key" 122 | (with-slots (public-key) key 123 | (key-bits public-key))) 124 | 125 | (defmethod generate-key-pair ((kind (eql :ecdsa-nistp384)) &key comment passphrase) 126 | "Generates a new pair of ECDSA NIST P-384 public and private keys" 127 | (let* ((cipher-name (if passphrase *default-cipher-name* "none")) 128 | (kdf-name (if passphrase "bcrypt" "none")) 129 | (key-type (get-key-type-or-lose :ecdsa-sha2-nistp384 :by :id)) 130 | (checksum-int (ironclad:random-bits 32)) 131 | (priv-pub-pair (multiple-value-list (ironclad:generate-key-pair :secp384r1))) 132 | (ironclad-priv-key (first priv-pub-pair)) 133 | (ironclad-pub-key (second priv-pub-pair)) 134 | 135 | ;; The private and public keys are actually `mpint` values. 136 | ;; However `ironclad` represents them as a raw bytes array, 137 | ;; and we are internally keeping them this way here as well. 138 | ;; Since `mpint` values according to RFC 4251 may have a sign, 139 | ;; we need to first decode the values as returned by `ironclad`, 140 | ;; then get their representation as `mpint` values, so we 141 | ;; can properly encode them back. 142 | (x-bytes (ironclad:secp384r1-key-x ironclad-priv-key)) 143 | (x-scalar (ironclad::ec-decode-scalar :secp384r1 x-bytes)) 144 | (x-stream (rfc4251:make-binary-output-stream)) 145 | (x-size (rfc4251:encode :mpint x-scalar x-stream)) 146 | 147 | (y-bytes (ironclad:secp384r1-key-y ironclad-pub-key)) 148 | (y-scalar (ironclad::ec-decode-scalar :secp384r1 y-bytes)) 149 | (y-stream (rfc4251:make-binary-output-stream)) 150 | (y-size (rfc4251:encode :mpint y-scalar y-stream)) 151 | 152 | (pub-key (make-instance 'ecdsa-nistp384-public-key 153 | :kind key-type 154 | :comment comment 155 | :identifier +nistp384-identifier+ 156 | :y (rfc4251:get-binary-stream-bytes y-stream))) 157 | (priv-key (make-instance 'ecdsa-nistp384-private-key 158 | :public-key pub-key 159 | :cipher-name cipher-name 160 | :kdf-name kdf-name 161 | :passphrase passphrase 162 | :checksum-int checksum-int 163 | :kind key-type 164 | :comment comment 165 | :identifier +nistp384-identifier+ 166 | :x (rfc4251:get-binary-stream-bytes x-stream) 167 | :y (rfc4251:get-binary-stream-bytes y-stream)))) 168 | (declare (ignore x-size y-size)) 169 | (values priv-key pub-key))) 170 | -------------------------------------------------------------------------------- /src/ecdsa-nistp521.lisp: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2020 Marin Atanasov Nikolov 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer 10 | ;; in this position and unchanged. 11 | ;; 2. Redistributions in binary form must reproduce the above copyright 12 | ;; notice, this list of conditions and the following disclaimer in the 13 | ;; documentation and/or other materials provided with the distribution. 14 | ;; 15 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | ;; IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | (in-package :cl-ssh-keys) 27 | 28 | (alexandria:define-constant +nistp521-identifier+ 29 | "nistp521" 30 | :test #'equal 31 | :documentation "NIST name of the curve") 32 | 33 | (defclass ecdsa-nistp521-public-key (base-ecdsa-nistp-public-key ironclad:secp521r1-public-key) 34 | () 35 | (:documentation "Represents an OpenSSH ECDSA NIST P-521 public key")) 36 | 37 | (defmethod rfc4251:decode ((type (eql :ecdsa-nistp521-public-key)) stream &key kind comment) 38 | "Decodes an ECDSA NIST P-521 public key from the given binary stream" 39 | (unless kind 40 | (error 'invalid-key-error 41 | :description "Public key kind was not specified")) 42 | ;; The `[identifier]` and `Q` fields as defined in RFC 5656, section 3.1. 43 | ;; The `Q` field is the public key, which is represented as `y` in 44 | ;; ironclad:secp521r1 class. This is an `mpint` value. 45 | ;; See https://tools.ietf.org/search/rfc4492#appendix-A for the 46 | ;; various names under which NIST P-521 curves are known. 47 | (let* ((identifier-data (multiple-value-list (rfc4251:decode :string stream))) ;; Identifier of elliptic curve domain parameters 48 | (q-data (multiple-value-list (rfc4251:decode :buffer stream))) ;; Public key 49 | (size (+ (second identifier-data) (second q-data))) ;; Total number of bytes read from the stream 50 | (pk (make-instance 'ecdsa-nistp521-public-key 51 | :kind kind 52 | :comment comment 53 | :identifier (first identifier-data) 54 | :y (first q-data)))) 55 | (values pk size))) 56 | 57 | (defmethod rfc4251:encode ((type (eql :ecdsa-nistp521-public-key)) (key ecdsa-nistp521-public-key) stream &key) 58 | "Encodes the ECDSA NIST P-521 public key into the given binary stream." 59 | (with-accessors ((identifier ecdsa-curve-identifier) (y ironclad:secp521r1-key-y)) key 60 | (+ 61 | (rfc4251:encode :string identifier stream) 62 | (rfc4251:encode :buffer y stream)))) 63 | 64 | (defmethod key-bits ((key ecdsa-nistp521-public-key)) 65 | "Returns the number of bits for the ECDSA NIST P-521 public key" 66 | ironclad::+secp521r1-bits+) 67 | 68 | (defclass ecdsa-nistp521-private-key (base-ecdsa-nistp-private-key ironclad:secp521r1-private-key) 69 | () 70 | (:documentation "Represents an OpenSSH ECDSA NIST P-521 private key")) 71 | 72 | (defmethod rfc4251:decode ((type (eql :ecdsa-nistp521-private-key)) stream &key kind public-key 73 | cipher-name kdf-name 74 | kdf-salt kdf-rounds 75 | passphrase checksum-int) 76 | "Decodes a ECDSA NIST P-521 private key from the given stream" 77 | (let* (identifier ;; Curve identifier 78 | d ;; Private key 79 | q) ;; Public key 80 | ;; Decode curve identifier. The decoded identifier must match with the 81 | ;; curve identifier. 82 | (setf identifier (rfc4251:decode :string stream)) 83 | (unless (string= identifier +nistp521-identifier+) 84 | (error 'invalid-key-error 85 | :description "Invalid ECDSA NIST P-521 key. Curve identifiers mismatch")) 86 | 87 | ;; Public key, also embedded in the encrypted section. Must match with the 88 | ;; one of the provided public key. 89 | (setf q (rfc4251:decode :buffer stream)) 90 | (unless (equalp q (ironclad:secp521r1-key-y public-key)) 91 | (error 'invalid-key-error 92 | :description "Invalid ECDSA NIST P-521 key. Decoded and provided public keys mismatch")) 93 | 94 | ;; Decode private key 95 | (setf d (rfc4251:decode :buffer stream)) 96 | 97 | (make-instance 'ecdsa-nistp521-private-key 98 | :kind kind 99 | :public-key public-key 100 | :cipher-name cipher-name 101 | :kdf-name kdf-name 102 | :kdf-salt kdf-salt 103 | :kdf-rounds kdf-rounds 104 | :passphrase passphrase 105 | :checksum-int checksum-int 106 | :identifier identifier 107 | :y q 108 | :x d))) 109 | 110 | (defmethod rfc4251:encode ((type (eql :ecdsa-nistp521-private-key)) (key ecdsa-nistp521-private-key) stream &key) 111 | "Encodes the ECDSA NIST P-521 private key into the given binary stream" 112 | (let ((identifier (ecdsa-curve-identifier key)) 113 | (y (ironclad:secp521r1-key-y key)) 114 | (x (ironclad:secp521r1-key-x key))) 115 | (+ 116 | (rfc4251:encode :string identifier stream) ;; Curve identifier 117 | (rfc4251:encode :buffer y stream) ;; Public key 118 | (rfc4251:encode :buffer x stream)))) ;; Private key 119 | 120 | (defmethod key-bits ((key ecdsa-nistp521-private-key)) 121 | "Returns the number of bits of the embedded public key" 122 | (with-slots (public-key) key 123 | (key-bits public-key))) 124 | 125 | (defmethod generate-key-pair ((kind (eql :ecdsa-nistp521)) &key comment passphrase) 126 | "Generates a new pair of ECDSA NIST P-521 public and private keys" 127 | (let* ((cipher-name (if passphrase *default-cipher-name* "none")) 128 | (kdf-name (if passphrase "bcrypt" "none")) 129 | (key-type (get-key-type-or-lose :ecdsa-sha2-nistp521 :by :id)) 130 | (checksum-int (ironclad:random-bits 32)) 131 | (priv-pub-pair (multiple-value-list (ironclad:generate-key-pair :secp521r1))) 132 | (ironclad-priv-key (first priv-pub-pair)) 133 | (ironclad-pub-key (second priv-pub-pair)) 134 | 135 | ;; The private and public keys are actually `mpint` values. 136 | ;; However `ironclad` represents them as a raw bytes array, 137 | ;; and we are internally keeping them this way here as well. 138 | ;; Since `mpint` values according to RFC 4251 may have a sign, 139 | ;; we need to first decode the values as returned by `ironclad`, 140 | ;; then get their representation as `mpint` values, so we 141 | ;; can properly encode them back. 142 | (x-bytes (ironclad:secp521r1-key-x ironclad-priv-key)) 143 | (x-scalar (ironclad::ec-decode-scalar :secp521r1 x-bytes)) 144 | (x-stream (rfc4251:make-binary-output-stream)) 145 | (x-size (rfc4251:encode :mpint x-scalar x-stream)) 146 | 147 | (y-bytes (ironclad:secp521r1-key-y ironclad-pub-key)) 148 | (y-scalar (ironclad::ec-decode-scalar :secp521r1 y-bytes)) 149 | (y-stream (rfc4251:make-binary-output-stream)) 150 | (y-size (rfc4251:encode :mpint y-scalar y-stream)) 151 | 152 | (pub-key (make-instance 'ecdsa-nistp521-public-key 153 | :kind key-type 154 | :comment comment 155 | :identifier +nistp521-identifier+ 156 | :y (rfc4251:get-binary-stream-bytes y-stream))) 157 | (priv-key (make-instance 'ecdsa-nistp521-private-key 158 | :public-key pub-key 159 | :cipher-name cipher-name 160 | :kdf-name kdf-name 161 | :passphrase passphrase 162 | :checksum-int checksum-int 163 | :kind key-type 164 | :comment comment 165 | :identifier +nistp521-identifier+ 166 | :x (rfc4251:get-binary-stream-bytes x-stream) 167 | :y (rfc4251:get-binary-stream-bytes y-stream)))) 168 | (declare (ignore x-size y-size)) 169 | (values priv-key pub-key))) 170 | -------------------------------------------------------------------------------- /src/rsa.lisp: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2020 Marin Atanasov Nikolov 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer 10 | ;; in this position and unchanged. 11 | ;; 2. Redistributions in binary form must reproduce the above copyright 12 | ;; notice, this list of conditions and the following disclaimer in the 13 | ;; documentation and/or other materials provided with the distribution. 14 | ;; 15 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | ;; IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | (in-package :cl-ssh-keys) 27 | 28 | (defclass rsa-public-key (base-public-key ironclad:rsa-public-key) 29 | () 30 | (:documentation "Represents an OpenSSH RSA public key")) 31 | 32 | (defmethod rfc4251:decode ((type (eql :rsa-public-key)) stream &key kind comment) 33 | "Decodes an RSA public key from the given binary stream" 34 | ;; RFC4251:DECODE returns multiple values -- the first one is the 35 | ;; decoded value and the second one is the number of bytes that were 36 | ;; read from the stream in order to produce the given value. 37 | ;; We need to return both of these in order to conform to the 38 | ;; interface defined by RFC4251:DECODE generic function. 39 | (unless kind 40 | (error 'invalid-key-error 41 | :description "Public key kind was not specified")) 42 | (let* ((e-data (multiple-value-list (rfc4251:decode :mpint stream))) ;; RSA public exponent 43 | (n-data (multiple-value-list (rfc4251:decode :mpint stream))) ;; RSA modulus 44 | (pk (make-instance 'rsa-public-key 45 | :e (first e-data) 46 | :n (first n-data) 47 | :kind kind 48 | :comment comment))) 49 | (values 50 | pk 51 | (+ (second e-data) (second n-data))))) 52 | 53 | (defmethod rfc4251:encode ((type (eql :rsa-public-key)) (key rsa-public-key) stream &key) 54 | "Encodes the RSA public key into the given binary stream." 55 | (with-accessors ((e ironclad:rsa-key-exponent) (n ironclad:rsa-key-modulus)) key 56 | (+ 57 | (rfc4251:encode :mpint e stream) ;; RSA public exponent 58 | (rfc4251:encode :mpint n stream)))) ;; RSA modulus 59 | 60 | (defmethod key-bits ((key rsa-public-key)) 61 | "Returns the number of bits for the RSA public key" 62 | (with-accessors ((n ironclad:rsa-key-modulus)) key 63 | (integer-length n))) 64 | 65 | (defclass rsa-private-key (base-private-key ironclad:rsa-private-key) 66 | () 67 | (:documentation "Represents an OpenSSH RSA private key")) 68 | 69 | (defmethod verify-signature ((key rsa-public-key) message (signature signature) &key) 70 | "Verifies the message using the signature as described in RFC 4253, section 6.6" 71 | (let ((digest-spec (getf (signature-type signature) :digest)) 72 | (blob (signature-blob signature))) 73 | (rsassa-pkcs1-v1_5-verify key message blob digest-spec))) 74 | 75 | (defmethod rfc4251:decode ((type (eql :rsa-private-key)) stream &key kind public-key 76 | cipher-name kdf-name 77 | kdf-salt kdf-rounds 78 | passphrase checksum-int) 79 | "Decodes an RSA private key from the given stream" 80 | (let* (n ;; RSA modulus 81 | e ;; RSA public exponent 82 | d ;; RSA private exponent 83 | iqmp ;; RSA Inverse of Q Mod P 84 | p ;; RSA prime number 1 85 | q) ;; RSA prime number 2 86 | ;; Decode RSA modulus. 87 | ;; The modulus must match with the one from the 88 | ;; already decoded embedded public key. 89 | (setf n (rfc4251:decode :mpint stream)) 90 | (unless (= n (ironclad:rsa-key-modulus public-key)) 91 | (error 'invalid-key-error 92 | :description "Invalid RSA modulus found in encrypted section")) 93 | 94 | ;; RSA public exponent, also part of the encrypted section. 95 | ;; Must match with the one from the already decoded pubic key. 96 | (setf e (rfc4251:decode :mpint stream)) 97 | (unless (= e (ironclad:rsa-key-exponent public-key)) 98 | (error 'invalid-key-error 99 | :description "Invalid RSA public exponent found in encrypted section")) 100 | 101 | ;; RSA private exponent 102 | (setf d (rfc4251:decode :mpint stream)) 103 | 104 | ;; Inverse of Q Mod P, a.k.a iqmp 105 | (setf iqmp (rfc4251:decode :mpint stream)) 106 | 107 | ;; RSA prime number 1 108 | (setf p (rfc4251:decode :mpint stream)) 109 | 110 | ;; RSA prime number 2 111 | (setf q (rfc4251:decode :mpint stream)) 112 | 113 | ;; Verify the CRT coefficient 114 | (unless (= iqmp (ironclad::modular-inverse-with-blinding q p)) 115 | (error 'invalid-key-error 116 | :description "Invalid CRT coefficient found in private key blob")) 117 | 118 | ;; We are good, if we've reached so far. 119 | (make-instance 'rsa-private-key 120 | :kind kind 121 | :public-key public-key 122 | :cipher-name cipher-name 123 | :kdf-name kdf-name 124 | :kdf-salt kdf-salt 125 | :kdf-rounds kdf-rounds 126 | :checksum-int checksum-int 127 | :passphrase passphrase 128 | :d d 129 | :n n 130 | :p p 131 | :q q))) 132 | 133 | (defmethod rfc4251:encode ((type (eql :rsa-private-key)) (key rsa-private-key) stream &key) 134 | "Encodes the RSA private key into the given binary stream" 135 | (let* ((public-key (embedded-public-key key)) 136 | (n (ironclad:rsa-key-modulus public-key)) 137 | (e (ironclad:rsa-key-exponent public-key)) 138 | (d (ironclad:rsa-key-exponent key)) 139 | (p (ironclad:rsa-key-prime-p key)) 140 | (q (ironclad:rsa-key-prime-q key)) 141 | (iqmp (ironclad::modular-inverse-with-blinding q p))) 142 | (+ 143 | (rfc4251:encode :mpint n stream) 144 | (rfc4251:encode :mpint e stream) 145 | (rfc4251:encode :mpint d stream) 146 | (rfc4251:encode :mpint iqmp stream) 147 | (rfc4251:encode :mpint p stream) 148 | (rfc4251:encode :mpint q stream)))) 149 | 150 | (defmethod key-bits ((key rsa-private-key)) 151 | "Returns the number of bits of the embedded public key" 152 | (with-slots (public-key) key 153 | (integer-length (ironclad:rsa-key-modulus public-key)))) 154 | 155 | (defmethod generate-key-pair ((kind (eql :rsa)) &key 156 | (num-bits 3072) 157 | comment 158 | passphrase) 159 | "Generates a new pair of RSA public and private keys" 160 | (when (< num-bits 1024) 161 | (error 'unsupported-key-error :description "Minimum key length for RSA keys is 1024")) 162 | (let* ((cipher-name (if passphrase *default-cipher-name* "none")) 163 | (kdf-name (if passphrase "bcrypt" "none")) 164 | (key-type (get-key-type-or-lose :ssh-rsa :by :id)) 165 | (checksum-int (ironclad:random-bits 32)) 166 | (priv-pub-pair (multiple-value-list (ironclad:generate-key-pair :rsa :num-bits num-bits))) 167 | (ironclad-priv-key (first priv-pub-pair)) 168 | (ironclad-pub-key (second priv-pub-pair)) 169 | (pub-key (make-instance 'rsa-public-key 170 | :e (ironclad:rsa-key-exponent ironclad-pub-key) 171 | :n (ironclad:rsa-key-modulus ironclad-pub-key) 172 | :kind key-type 173 | :comment comment)) 174 | (priv-key (make-instance 'rsa-private-key 175 | :public-key pub-key 176 | :cipher-name cipher-name 177 | :kdf-name kdf-name 178 | :passphrase passphrase 179 | :checksum-int checksum-int 180 | :kind key-type 181 | :comment comment 182 | :d (ironclad:rsa-key-exponent ironclad-priv-key) 183 | :n (ironclad:rsa-key-modulus ironclad-pub-key) 184 | :p (ironclad:rsa-key-prime-p ironclad-priv-key) 185 | :q (ironclad:rsa-key-prime-q ironclad-priv-key)))) 186 | (values priv-key pub-key))) 187 | -------------------------------------------------------------------------------- /src/ecdsa-nistp256.lisp: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2020 Marin Atanasov Nikolov 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer 10 | ;; in this position and unchanged. 11 | ;; 2. Redistributions in binary form must reproduce the above copyright 12 | ;; notice, this list of conditions and the following disclaimer in the 13 | ;; documentation and/or other materials provided with the distribution. 14 | ;; 15 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | ;; IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | (in-package :cl-ssh-keys) 27 | 28 | (alexandria:define-constant +nistp256-identifier+ 29 | "nistp256" 30 | :test #'equal 31 | :documentation "NIST name of the curve") 32 | 33 | (defclass ecdsa-nistp256-public-key (base-ecdsa-nistp-public-key ironclad:secp256r1-public-key) 34 | () 35 | (:documentation "Represents an OpenSSH ECDSA NIST P-256 public key")) 36 | 37 | (defmethod verify-signature ((key base-ecdsa-nistp-public-key) message (signature signature) &key) 38 | "Verifies the signature of the message according to RFC 5656" 39 | ;; The `r' and `s' signature parameters are part of the signature 40 | ;; blob, so we need to decode them first. See RFC 5656, section 41 | ;; 3.1.2 for more details. 42 | (rfc4251:with-binary-input-stream (stream (signature-blob signature)) 43 | (let* ((n-bits (key-bits key)) 44 | (r (rfc4251:decode :mpint stream)) 45 | (s (rfc4251:decode :mpint stream)) 46 | (r-bytes (ironclad:integer-to-octets r :n-bits n-bits)) 47 | (s-bytes (ironclad:integer-to-octets s :n-bits n-bits)) 48 | (digest-spec (getf (signature-type signature) :digest)) 49 | (ecdsa-signature (concatenate '(simple-array (unsigned-byte 8) (*)) r-bytes s-bytes))) 50 | (ironclad:verify-signature key (ironclad:digest-sequence digest-spec message) ecdsa-signature)))) 51 | 52 | (defmethod rfc4251:decode ((type (eql :ecdsa-nistp256-public-key)) stream &key kind comment) 53 | "Decodes an ECDSA NIST P-256 public key from the given binary stream" 54 | (unless kind 55 | (error 'invalid-key-error 56 | :description "Public key kind was not specified")) 57 | ;; The `[identifier]` and `Q` fields as defined in RFC 5656, section 3.1. 58 | ;; The `Q` field is the public key, which is represented as `y` in 59 | ;; ironclad:secp256r1 class. This is an `mpint` value. 60 | ;; See https://tools.ietf.org/search/rfc4492#appendix-A for the 61 | ;; various names under which NIST P-256 curves are known. 62 | (let* ((identifier-data (multiple-value-list (rfc4251:decode :string stream))) ;; Identifier of elliptic curve domain parameters 63 | (q-data (multiple-value-list (rfc4251:decode :buffer stream))) ;; Public key 64 | (size (+ (second identifier-data) (second q-data))) ;; Total number of bytes read from the stream 65 | (pk (make-instance 'ecdsa-nistp256-public-key 66 | :kind kind 67 | :comment comment 68 | :identifier (first identifier-data) 69 | :y (first q-data)))) 70 | (values pk size))) 71 | 72 | (defmethod rfc4251:encode ((type (eql :ecdsa-nistp256-public-key)) (key ecdsa-nistp256-public-key) stream &key) 73 | "Encodes the ECDSA NIST P-256 public key into the given binary stream." 74 | (with-accessors ((identifier ecdsa-curve-identifier) (y ironclad:secp256r1-key-y)) key 75 | (+ 76 | (rfc4251:encode :string identifier stream) 77 | (rfc4251:encode :buffer y stream)))) 78 | 79 | (defmethod key-bits ((key ecdsa-nistp256-public-key)) 80 | "Returns the number of bits for the ECDSA NIST P-256 public key" 81 | ironclad::+secp256r1-bits+) 82 | 83 | (defclass ecdsa-nistp256-private-key (base-ecdsa-nistp-private-key ironclad:secp256r1-private-key) 84 | () 85 | (:documentation "Represents an OpenSSH ECDSA NIST P-256 private key")) 86 | 87 | (defmethod rfc4251:decode ((type (eql :ecdsa-nistp256-private-key)) stream &key kind public-key 88 | cipher-name kdf-name 89 | kdf-salt kdf-rounds 90 | passphrase checksum-int) 91 | "Decodes a ECDSA NIST P-256 private key from the given stream" 92 | (let* (identifier ;; Curve identifier 93 | d ;; Private key 94 | q) ;; Public key 95 | ;; Decode curve identifier. The decoded identifier must match with the 96 | ;; curve identifier. 97 | (setf identifier (rfc4251:decode :string stream)) 98 | (unless (string= identifier +nistp256-identifier+) 99 | (error 'invalid-key-error 100 | :description "Invalid ECDSA NIST P-256 key. Curve identifiers mismatch")) 101 | 102 | ;; Public key, also embedded in the encrypted section. Must match with the 103 | ;; one of the provided public key. 104 | (setf q (rfc4251:decode :buffer stream)) 105 | (unless (equalp q (ironclad:secp256r1-key-y public-key)) 106 | (error 'invalid-key-error 107 | :description "Invalid ECDSA NIST P-256 key. Decoded and provided public keys mismatch")) 108 | 109 | ;; Decode private key 110 | (setf d (rfc4251:decode :buffer stream)) 111 | 112 | (make-instance 'ecdsa-nistp256-private-key 113 | :kind kind 114 | :public-key public-key 115 | :cipher-name cipher-name 116 | :kdf-name kdf-name 117 | :kdf-salt kdf-salt 118 | :kdf-rounds kdf-rounds 119 | :passphrase passphrase 120 | :checksum-int checksum-int 121 | :identifier identifier 122 | :y q 123 | :x d))) 124 | 125 | (defmethod rfc4251:encode ((type (eql :ecdsa-nistp256-private-key)) (key ecdsa-nistp256-private-key) stream &key) 126 | "Encodes the ECDSA NIST P-256 private key into the given binary stream" 127 | (let ((identifier (ecdsa-curve-identifier key)) 128 | (y (ironclad:secp256r1-key-y key)) 129 | (x (ironclad:secp256r1-key-x key))) 130 | (+ 131 | (rfc4251:encode :string identifier stream) ;; Curve identifier 132 | (rfc4251:encode :buffer y stream) ;; Public key 133 | (rfc4251:encode :buffer x stream)))) ;; Private key 134 | 135 | (defmethod key-bits ((key ecdsa-nistp256-private-key)) 136 | "Returns the number of bits of the embedded public key" 137 | (with-slots (public-key) key 138 | (key-bits public-key))) 139 | 140 | (defmethod generate-key-pair ((kind (eql :ecdsa-nistp256)) &key comment passphrase) 141 | "Generates a new pair of ECDSA NIST P-256 public and private keys" 142 | (let* ((cipher-name (if passphrase *default-cipher-name* "none")) 143 | (kdf-name (if passphrase "bcrypt" "none")) 144 | (key-type (get-key-type-or-lose :ecdsa-sha2-nistp256 :by :id)) 145 | (checksum-int (ironclad:random-bits 32)) 146 | (priv-pub-pair (multiple-value-list (ironclad:generate-key-pair :secp256r1))) 147 | (ironclad-priv-key (first priv-pub-pair)) 148 | (ironclad-pub-key (second priv-pub-pair)) 149 | 150 | ;; The private and public keys are actually `mpint` values. 151 | ;; However `ironclad` represents them as a raw bytes array, 152 | ;; and we are internally keeping them this way here as well. 153 | ;; Since `mpint` values according to RFC 4251 may have a sign, 154 | ;; we need to first decode the values as returned by `ironclad`, 155 | ;; then get their representation as `mpint` values, so we 156 | ;; can properly encode them back. 157 | (x-bytes (ironclad:secp256r1-key-x ironclad-priv-key)) 158 | (x-scalar (ironclad::ec-decode-scalar :secp256r1 x-bytes)) 159 | (x-stream (rfc4251:make-binary-output-stream)) 160 | (x-size (rfc4251:encode :mpint x-scalar x-stream)) 161 | 162 | (y-bytes (ironclad:secp256r1-key-y ironclad-pub-key)) 163 | (y-scalar (ironclad::ec-decode-scalar :secp256r1 y-bytes)) 164 | (y-stream (rfc4251:make-binary-output-stream)) 165 | (y-size (rfc4251:encode :mpint y-scalar y-stream)) 166 | 167 | (pub-key (make-instance 'ecdsa-nistp256-public-key 168 | :kind key-type 169 | :comment comment 170 | :identifier +nistp256-identifier+ 171 | :y (rfc4251:get-binary-stream-bytes y-stream))) 172 | (priv-key (make-instance 'ecdsa-nistp256-private-key 173 | :public-key pub-key 174 | :cipher-name cipher-name 175 | :kdf-name kdf-name 176 | :passphrase passphrase 177 | :checksum-int checksum-int 178 | :kind key-type 179 | :comment comment 180 | :identifier +nistp256-identifier+ 181 | :x (rfc4251:get-binary-stream-bytes x-stream) 182 | :y (rfc4251:get-binary-stream-bytes y-stream)))) 183 | (declare (ignore x-size y-size)) 184 | (values priv-key pub-key))) 185 | -------------------------------------------------------------------------------- /src/public-key.lisp: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2020 Marin Atanasov Nikolov 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer 10 | ;; in this position and unchanged. 11 | ;; 2. Redistributions in binary form must reproduce the above copyright 12 | ;; notice, this list of conditions and the following disclaimer in the 13 | ;; documentation and/or other materials provided with the distribution. 14 | ;; 15 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | ;; IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | (in-package :cl-ssh-keys) 27 | 28 | (defclass base-public-key (base-key) 29 | () 30 | (:documentation "Base class for representing an OpenSSH public key")) 31 | 32 | (defclass base-ecdsa-nistp-public-key (base-ecdsa-nistp-key base-public-key) 33 | () 34 | (:documentation "Base class for representing an OpenSSH ECDSA public key")) 35 | 36 | (defmethod rfc4251:decode ((type (eql :public-key)) stream &key key-type-name comment) 37 | "Decode a public key from the given stream. If KEY-TYPE-NAME is specified 38 | then we dispatch decoding to the respective implementation of the given 39 | KEY-TYPE-NAME (e.g. ssh-rsa). Otherwise the KEY-TYPE-NAME is first decoded 40 | from the binary stream and then dispatched to the respective implementation." 41 | (let* ((total 0) ;; Total number of bytes decoded 42 | key-type 43 | key-id) 44 | ;; Decode key type, if not specified explicitely 45 | (unless key-type-name 46 | (multiple-value-bind (value size) (rfc4251:decode :string stream) 47 | (incf total size) 48 | (setf key-type-name value))) 49 | 50 | (setf key-type (get-key-type-or-lose key-type-name :by :name)) 51 | (setf key-id (getf key-type :id)) 52 | 53 | (multiple-value-bind (pub-key size) 54 | (case key-id 55 | (:ssh-rsa (rfc4251:decode :rsa-public-key stream :kind key-type :comment comment)) 56 | (:ssh-dss (rfc4251:decode :dsa-public-key stream :kind key-type :comment comment)) 57 | (:ssh-ed25519 (rfc4251:decode :ed25519-public-key stream :kind key-type :comment comment)) 58 | (:ecdsa-sha2-nistp256 (rfc4251:decode :ecdsa-nistp256-public-key stream :kind key-type :comment comment)) 59 | (:ecdsa-sha2-nistp384 (rfc4251:decode :ecdsa-nistp384-public-key stream :kind key-type :comment comment)) 60 | (:ecdsa-sha2-nistp521 (rfc4251:decode :ecdsa-nistp521-public-key stream :kind key-type :comment comment)) 61 | ;; Cert keys 62 | (:ssh-rsa-cert-v01 (rfc4251:decode :ssh-cert-key stream :kind key-type :comment comment)) 63 | (:ssh-dss-cert-v01 (rfc4251:decode :ssh-cert-key stream :kind key-type :comment comment)) 64 | (:ecdsa-sha2-nistp256-cert-v01 (rfc4251:decode :ssh-cert-key stream :kind key-type :comment comment)) 65 | (:ecdsa-sha2-nistp384-cert-v01 (rfc4251:decode :ssh-cert-key stream :kind key-type :comment comment)) 66 | (:ecdsa-sha2-nistp521-cert-v01 (rfc4251:decode :ssh-cert-key stream :kind key-type :comment comment)) 67 | (:ssh-ed25519-cert-v01 (rfc4251:decode :ssh-cert-key stream :kind key-type :comment comment)) 68 | (t 69 | (error 'invalid-key-error 70 | :description (format nil "Unknown key type ~a" key-type-name)))) 71 | (values pub-key (+ total size))))) 72 | 73 | (defmethod rfc4251:encode ((type (eql :public-key)) (key base-public-key) stream &key (encode-key-type-p t)) 74 | "Encodes the public key into the binary stream according to RFC 4253, section 6.6. 75 | If ENCODE-KEY-TYPE-P is T, then the key type name (e.g. ssh-rsa) is 76 | encoded in the stream as well, before the actual public key components. 77 | Some key types (e.g. OpenSSH certificate keys) do not encode the key 78 | type name, when being embedded within a certificate." 79 | (let* ((kind (key-kind key)) 80 | (key-id (getf kind :id)) 81 | (key-type-name (getf kind :name))) 82 | ;; The number of bytes written should be the sum of the 83 | ;; key-type name and the public key components 84 | (+ 85 | (if encode-key-type-p 86 | (rfc4251:encode :string key-type-name stream) 87 | 0) ;; No key type name being encoded, so return 0 bytes written here. 88 | 89 | (case key-id 90 | (:ssh-rsa (rfc4251:encode :rsa-public-key key stream)) 91 | (:ssh-dss (rfc4251:encode :dsa-public-key key stream)) 92 | (:ssh-ed25519 (rfc4251:encode :ed25519-public-key key stream)) 93 | (:ecdsa-sha2-nistp256 (rfc4251:encode :ecdsa-nistp256-public-key key stream)) 94 | (:ecdsa-sha2-nistp384 (rfc4251:encode :ecdsa-nistp384-public-key key stream)) 95 | (:ecdsa-sha2-nistp521 (rfc4251:encode :ecdsa-nistp521-public-key key stream)) 96 | ;; Cert keys 97 | (:ssh-rsa-cert-v01 (rfc4251:encode :ssh-cert-key key stream)) 98 | (:ssh-dss-cert-v01 (rfc4251:encode :ssh-cert-key key stream)) 99 | (:ecdsa-sha2-nistp256-cert-v01 (rfc4251:encode :ssh-cert-key key stream)) 100 | (:ecdsa-sha2-nistp384-cert-v01 (rfc4251:encode :ssh-cert-key key stream)) 101 | (:ecdsa-sha2-nistp521-cert-v01 (rfc4251:encode :ssh-cert-key key stream)) 102 | (:ssh-ed25519-cert-v01 (rfc4251:encode :ssh-cert-key key stream)) 103 | (t 104 | (error 'invalid-key-error 105 | :description (format nil "Unknown key type ~a" key-type-name))))))) 106 | 107 | (defmethod fingerprint ((hash-spec (eql :md5)) (key base-public-key) &key) 108 | "Computes the MD5 fingerprint of the given public key" 109 | (let* ((stream (rfc4251:make-binary-output-stream)) 110 | (size (rfc4251:encode :public-key key stream)) 111 | (bytes (rfc4251:get-binary-stream-bytes stream)) 112 | (digest (ironclad:digest-sequence :md5 bytes))) 113 | (declare (ignore size)) 114 | (format nil "~{~(~2,'0x~)~^:~}" (coerce digest 'list)))) 115 | 116 | (defmethod fingerprint ((hash-spec (eql :sha1)) (key base-public-key) &key) 117 | "Computes the SHA-1 fingerprint of the given public key" 118 | (let* ((stream (rfc4251:make-binary-output-stream)) 119 | (size (rfc4251:encode :public-key key stream)) 120 | (bytes (rfc4251:get-binary-stream-bytes stream)) 121 | (digest (ironclad:digest-sequence :sha1 bytes)) 122 | (encoded (cl-base64:usb8-array-to-base64-string digest)) 123 | (trim-position (position #\= encoded :test #'char=))) ;; Trim padding characters 124 | (declare (ignore size)) 125 | (subseq encoded 0 trim-position))) 126 | 127 | (defmethod fingerprint ((hash-spec (eql :sha256)) (key base-public-key) &key) 128 | "Computes the SHA-256 fingerprint of the given public key" 129 | (let* ((stream (rfc4251:make-binary-output-stream)) 130 | (size (rfc4251:encode :public-key key stream)) 131 | (bytes (rfc4251:get-binary-stream-bytes stream)) 132 | (digest (ironclad:digest-sequence :sha256 bytes)) 133 | (encoded (cl-base64:usb8-array-to-base64-string digest)) 134 | (trim-position (position #\= encoded :test #'char=))) ;; Trim padding characters 135 | (declare (ignore size)) 136 | (subseq encoded 0 trim-position))) 137 | 138 | (defmethod write-key ((key base-public-key) &optional (stream *standard-output*)) 139 | "Writes the public key in its text representation" 140 | (let* ((s (rfc4251:make-binary-output-stream)) 141 | (size (rfc4251:encode :public-key key s)) 142 | (data (rfc4251:get-binary-stream-bytes s)) 143 | (encoded (cl-base64:usb8-array-to-base64-string data)) 144 | (key-type-name (getf (key-kind key) :name)) 145 | (comment (key-comment key))) 146 | (declare (ignore size)) 147 | (format stream "~a ~a" key-type-name encoded) 148 | (when comment 149 | (format stream " ~a" comment)) 150 | (format stream "~&"))) 151 | 152 | (defmacro with-public-key ((var text) &body body) 153 | "Parses a public key from the given TEXT and evaluates the 154 | BODY with VAR bound to the decoded public key" 155 | `(let ((,var (parse-public-key ,text))) 156 | ,@body)) 157 | 158 | (defmacro with-public-key-file ((var path) &body body) 159 | "Parses a public key from the given PATH and evaluates the 160 | BODY with VAR bound to the decoded public key" 161 | `(let ((,var (parse-public-key-file ,path))) 162 | ,@body)) 163 | 164 | (defun parse-public-key (text) 165 | "Parses an OpenSSH public key from the given plain-text string" 166 | (let* ((parts (uiop:split-string text :separator '(#\Space))) 167 | (key-type (get-key-type-or-lose (first parts) :by :name)) 168 | (data (second parts)) 169 | (comment (third parts))) 170 | ;; OpenSSH public keys are encoded in a way, so that the 171 | ;; key kind preceeds the actual public key components. 172 | ;; See RFC 4253 for more details. 173 | (let* ((stream (rfc4251:make-binary-input-stream (cl-base64:base64-string-to-usb8-array data))) 174 | (key-type-name (getf key-type :name)) 175 | (encoded-key-type-name (rfc4251:decode :string stream))) 176 | (unless (string= key-type-name encoded-key-type-name) 177 | (error 'key-type-mismatch-error 178 | :description "Key types mismatch" 179 | :expected key-type-name 180 | :found encoded-key-type-name)) 181 | (multiple-value-bind (key size) (rfc4251:decode :public-key 182 | stream 183 | :key-type-name key-type-name 184 | :comment comment) 185 | (declare (ignore size)) 186 | key)))) 187 | 188 | (defun parse-public-key-file (path) 189 | "Parses an OpenSSH public key from the given path" 190 | (with-open-file (in path) 191 | (parse-public-key (read-line in)))) 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## cl-ssh-keys 2 | 3 | `cl-ssh-keys` is a Common Lisp system, which provides the following 4 | features. 5 | 6 | * Decode OpenSSH public keys as defined in [RFC 4253][RFC 4253], 7 | section 6.6. 8 | * Decode OpenSSH private private keys as defined in 9 | [PROTOCOL.key][PROTOCOL.key] 10 | * Generate new private/public key pairs in OpenSSH compatible 11 | binary format. 12 | 13 | ## Requirements 14 | 15 | * [Quicklisp][Quicklisp] 16 | 17 | ## Installation 18 | 19 | Clone the [cl-ssh-keys][cl-ssh-keys] repo in 20 | your [Quicklisp local-projects 21 | directory][Quicklisp FAQ]. 22 | 23 | ``` shell 24 | git clone https://github.com/dnaeon/cl-ssh-keys.git 25 | ``` 26 | 27 | Load the system. 28 | 29 | ``` common-lisp 30 | CL-USER> (ql:quickload :cl-ssh-keys) 31 | ``` 32 | 33 | ## Supported Key Types 34 | 35 | The following public and private key pairs can be decoded, encoded and 36 | generated by `cl-ssh-keys`. 37 | 38 | | Type | Status | 39 | |---------|-----------| 40 | | RSA | Supported | 41 | | DSA | Supported | 42 | | ED25519 | Supported | 43 | | ECDSA | Supported | 44 | 45 | In addition to the public keys listed above the following certificate 46 | key types are supported. 47 | 48 | | Type | Status | 49 | |------------------------------------------|-----------| 50 | | ssh-rsa-cert-v01@openssh.com | Supported | 51 | | ssh-dss-cert-v01@openssh.com | Supported | 52 | | ecdsa-sha2-nistp256-cert-v01@openssh.com | Supported | 53 | | ecdsa-sha2-nistp384-cert-v01@openssh.com | Supported | 54 | | ecdsa-sha2-nistp521-cert-v01@openssh.com | Supported | 55 | | ssh-ed25519-cert-v01@openssh.com | Supported | 56 | 57 | ## Usage 58 | 59 | The following section provides various examples showing you how to decode, 60 | encode, and generate new OpenSSH private and public key pairs. 61 | 62 | For additional examples, make sure to check the [test 63 | suite](./t/test-suite.lisp). 64 | 65 | ### Public keys 66 | 67 | A public key can be parsed from a given string using the 68 | `SSH-KEYS:PARSE-PUBLIC-KEY` function, or from a file using the 69 | `SSH-KEYS:PARSE-PUBLIC-KEY-FILE` function. 70 | 71 | The public key may be a regular public key (e.g. RSA, DSA, etc.), or 72 | it could be an [OpenSSH Certificate 73 | Key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys). 74 | 75 | ``` common-lisp 76 | CL-USER> (defparameter *public-key* 77 | (ssh-keys:parse-public-key-file #P"~/.ssh/id_rsa.pub")) 78 | *PUBLIC-KEY* 79 | ``` 80 | 81 | You can retrieve the comment associated with a public key by using the 82 | `SSH-KEYS:KEY-COMMENT` accessor. 83 | 84 | ``` common-lisp 85 | CL-USER> (ssh-keys:key-comment *public-key*) 86 | "john.doe@localhost" 87 | ``` 88 | 89 | The key kind can be retrieved using `SSH-KEYS:KEY-KIND`. 90 | 91 | ``` common-lisp 92 | CL-USER> (ssh-keys:key-kind *public-key*) 93 | (:NAME "ssh-rsa" :PLAIN-NAME "ssh-rsa" :SHORT-NAME "RSA" :ID :SSH-RSA :IS-CERT NIL) 94 | ``` 95 | 96 | The number of bits for a key can be retrieved using the 97 | `SSH-KEYS:KEY-BITS` generic function, e.g. 98 | 99 | ``` common-lisp 100 | CL-USER> (ssh-keys:key-bits *public-key*) 101 | 3072 102 | ``` 103 | 104 | `SSH-KEYS:WITH-PUBLIC-KEY` and `SSH-KEYS:WITH-PUBLIC-KEY-FILE` 105 | are convenient macros when working with public keys, e.g. 106 | 107 | ``` common-lisp 108 | CL-USER> (ssh-keys:with-public-key-file (key #P"~/.ssh/id_rsa.pub") 109 | (format t "Comment: ~a~%" (ssh-keys:key-comment key)) 110 | (format t "MD5 fingerprint: ~a~%" (ssh-keys:fingerprint :md5 key)) 111 | (format t "Number of bits: ~a~%" (ssh-keys:key-bits key))) 112 | Comment: john.doe@localhost 113 | MD5 fingerprint: 04:02:4b:b2:43:39:a4:8e:89:47:49:6f:30:78:94:1e 114 | Number of bits: 3072 115 | NIL 116 | ``` 117 | 118 | ### Private keys 119 | 120 | A private keys can be parsed using the `SSH-KEYS:PARSE-PRIVATE-KEY` 121 | function, which takes a string representing a private key in [OpenSSH 122 | private key format][PROTOCOL.key], or you can use the 123 | `SSH-KEYS:PARSE-PRIVATE-KEY-FILE` function, e.g. 124 | 125 | ``` common-lisp 126 | CL-USER> (defparameter *private-key* 127 | (ssh-keys:parse-private-key-file #P"~/.ssh/id_rsa")) 128 | *PRIVATE-KEY* 129 | ``` 130 | 131 | Key kind, comment and number of bits can be retrieved using 132 | `SSH-KEYS:KEY-KIND`, `SSH-KEYS:KEY-COMMENT` and `SSH-KEYS:KEY-BITS`, 133 | similarly to the way you would for public keys, e.g. 134 | 135 | ``` common-lisp 136 | CL-USER> (ssh-keys:key-kind *private-key*) 137 | (:NAME "ssh-rsa" :PLAIN-NAME "ssh-rsa" :SHORT-NAME "RSA" :ID :SSH-RSA :IS-CERT NIL) 138 | CL-USER> (ssh-keys:key-comment *private-key*) 139 | "john.doe@localhost" 140 | CL-USER> (ssh-keys:key-bits *private-key*) 141 | 3072 142 | ``` 143 | 144 | OpenSSH private keys embed the public key within the binary blob of 145 | the private key. From a private key you can get the embedded public 146 | key using `SSH-KEYS:EMBEDDED-PUBLIC-KEY`, e.g. 147 | 148 | ``` common-lisp 149 | CL-USER> (ssh-keys:embedded-public-key *private-key*) 150 | # 151 | ``` 152 | 153 | You can also use the `SSH-KEYS:WITH-PRIVATE-KEY` and 154 | `SSH-KEYS:WITH-PRIVATE-KEY-FILE` macros when working with private 155 | keys. 156 | 157 | ``` common-lisp 158 | CL-USER> (ssh-keys:with-private-key-file (key #P"~/.ssh/id_rsa") 159 | (format t "Comment: ~a~%" (ssh-keys:key-comment key)) 160 | (format t "MD5 fingerprint: ~a~%" (ssh-keys:fingerprint :md5 key))) 161 | Comment: john.doe@localhost 162 | MD5 fingerprint: 04:02:4b:b2:43:39:a4:8e:89:47:49:6f:30:78:94:1e 163 | ``` 164 | 165 | ### Encrypted keys 166 | 167 | In order to parse an encrypted private key you need to provide a 168 | passphrase, e.g. 169 | 170 | ``` common-lisp 171 | CL-USER> (ssh-keys:with-private-key-file (key #P"~/.ssh/id_rsa" :passphrase "my-secret-password") 172 | (ssh-keys:key-cipher-name key)) 173 | "aes256-ctr" 174 | ``` 175 | 176 | ### Changing passphrase of an encrypted key 177 | 178 | The passphrase for an encrypted private key can be changed by setting 179 | a new value for the passphrase using the `SSH-KEYS:KEY-PASSPHRASE` 180 | accessor. 181 | 182 | This example changes the passphrase for a given key and saves it on 183 | the filesystem. 184 | 185 | ``` common-lisp 186 | CL-USER> (ssh-keys:with-private-key-file (key #P"~/.ssh/id_rsa" :passphrase "OLD-PASSPHRASE") 187 | (setf (ssh-keys:key-passphrase key) "MY-NEW-PASSPHRASE") 188 | (ssh-keys:write-key-to-path key #P"~/.id_rsa-new-passphrase")) 189 | ``` 190 | 191 | ### Setting passphrase for an existing un-encrypted key 192 | 193 | In order to set a passphrase for an existing un-encrypted private key, 194 | simply set a passphrase using the `SSH-KEYS:KEY-PASSPHRASE` accessor, 195 | e.g. 196 | 197 | ``` common-lisp 198 | CL-USER> (ssh-keys:with-private-key-file (key #P"~/.ssh/id_rsa") 199 | (setf (ssh-keys:key-passphrase key) "my-secret-password") 200 | (ssh-keys:write-key-to-path key #P"~/.id_rsa-encrypted")) 201 | ``` 202 | 203 | ### Removing passphrase of an encrypted key 204 | 205 | You can remove the passphrase of a private key and make it 206 | un-encrypted by setting the passphrase to `nil`. 207 | 208 | ``` common-lisp 209 | CL-USER> (ssh-keys:with-private-key-file (key #P"~/.ssh/id_rsa" :passphrase "PASSPHRASE") 210 | (setf (ssh-keys:key-passphrase key) nil) 211 | (ssh-keys:write-key-to-path key #P"~/.id_rsa-unencrypted")) 212 | ``` 213 | 214 | ### Changing the cipher of an encrypted key 215 | 216 | The cipher to be used for encryption of a private key can be set by 217 | using the `SSH-KEYS:KEY-CIPHER-NAME` accessor. The value should be one 218 | of the known and supported ciphers as returned by 219 | `SSH-KEYS:GET-ALL-CIPHER-NAMES`. 220 | 221 | First, list the known cipher names. 222 | 223 | ``` common-lisp 224 | CL-USER> (ssh-keys:get-all-cipher-names) 225 | ("3des-cbc" "aes128-cbc" "aes192-cbc" "aes256-cbc" "aes128-ctr" "aes192-ctr" "aes256-ctr" "none") 226 | ``` 227 | 228 | Then set a new cipher. 229 | 230 | ``` common-lisp 231 | CL-USER> (ssh-keys:with-private-key-file (key #P"~/.ssh/id_rsa" :passphrase "PASSPHRASE") 232 | (setf (ssh-keys:key-cipher-name key) "3des-cbc") 233 | (ssh-keys:write-key-to-path key #P"~/.id_rsa-3des-cbc")) 234 | ``` 235 | 236 | ### Changing the KDF number of iterations 237 | 238 | By default `ssh-keygen(1)` and `cl-ssh-keys` will use `16` rounds of 239 | iterations in order to produce an encryption key. You can set this to 240 | a higher value, if needed, which would help against brute-force 241 | attacks. 242 | 243 | ``` common-lisp 244 | CL-USER> (ssh-keys:with-private-key-file (key #P"~/.ssh/id_rsa" :passphrase "PASSPHRASE") 245 | (setf (ssh-keys:key-kdf-rounds key) 32) 246 | (ssh-keys:write-key-to-path key #P"~/.id_rsa-stronger")) 247 | ``` 248 | 249 | ### Fingerprints 250 | 251 | Key fingerprints can be generated using the `SSH-KEYS:FINGERPRINT` 252 | generic function. 253 | 254 | The following examples show how to generate the SHA-256, SHA-1 and MD5 255 | fingerprints of a given public key. 256 | 257 | ``` common-lisp 258 | CL-USER> (ssh-keys:fingerprint :sha256 *public-key*) 259 | "VmYpd+5gvA5Cj57ZZcI8lnFMNNic6jpnnBd0WoNG1F8" 260 | CL-USER> (ssh-keys:fingerprint :sha1 *public-key*) 261 | "RnLPLG93GrABjOqc6xOvVFpQXsc" 262 | CL-USER> (ssh-keys:fingerprint :md5 *public-key*) 263 | "04:02:4b:b2:43:39:a4:8e:89:47:49:6f:30:78:94:1e" 264 | ``` 265 | 266 | Fingerprints of private keys are computed against the embedded public 267 | key. 268 | 269 | ### Writing Keys 270 | 271 | A public and private key can be written in its text representation 272 | using the `SSH-KEYS:WRITE-KEY` generic function. 273 | 274 | ``` common-lisp 275 | CL-USER> (ssh-keys:write-key *public-key*) 276 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCsngzCcay+lQ+34qUeUSH2m1ZYW9B0a2rxpMmvYFcOyL/hRPJwv8XO89T0+HQIZRC+xlM3BSqdFGs+B58MYXPvo3H+p00CJN8tUjvC3VD74kiXSNxIyhBpKCY1s58RxnWS/6bPQIYfnCVBiQZnkNe1T3isxND1Y71TnbSz5QN2xBkAtiGPH0dPM89yWbZpTjTCaIOfyZn2fBBsmp0zUgEJ7o9W9Lrxs1f0Pn+bZ4PqFSEUzlub7mAQ+RpwgGeLeWIFz+o6KQJPFiuRgzQU6ZsY+wjorVefzgeqpRiWGw/bEyUDck09B4B0IWoTtIiKRzd635nOo7Lz/1XgaMZ60WZD9T/labEWcKmtp4Y7NoCkep0DyYyoAgWrco4FD1r0g4WcVbsJQt8HzRy9UaHlh6YPY/xkk0bSiljpygEiT48FxniqE+6HY+7SbC1wz5QThY+UsIiDgFcg3BljskfT8Il3hateXI2wEXqww4+a+DxcHzypclYorbQKUzdzNLZRBNk= john.doe@localhost 277 | NIL 278 | ``` 279 | 280 | Another example, this time using a private key. 281 | 282 | ``` common-lisp 283 | CL-USER> (ssh-keys:write-key *private-key*) 284 | -----BEGIN OPENSSH PRIVATE KEY----- 285 | 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 321 | -----END OPENSSH PRIVATE KEY----- 322 | NIL 323 | ``` 324 | 325 | The `SSH-KEYS:WRITE-KEY` generic function takes an optional stream 326 | parameter, so you can write your keys to a given stream, if needed. 327 | 328 | ``` common-lisp 329 | CL-USER> (with-open-file (out #P"my-rsa-public-key" :direction :output) 330 | (ssh-keys:write-key *public-key* out)) 331 | NIL 332 | ``` 333 | 334 | `SSH-KEYS:WRITE-KEY-TO-PATH` is a convenience function you can use to 335 | write keys to a given path, e.g. 336 | 337 | ``` common-lisp 338 | CL-USER> (ssh-keys:write-key-to-path (key #P"my-rsa-public-key") 339 | ``` 340 | 341 | ### Generating new private/public key pairs 342 | 343 | The `SSH-KEYS:GENERATE-KEY-PAIR` generic function creates a new 344 | private/public key pair of a given kind. 345 | 346 | The generated keys are identical with what `ssh-keygen(1)` would 347 | produce and you can use them to authenticate to remote systems. 348 | 349 | The following example creates an RSA private/public key pair, and 350 | saves the keys on the file system. 351 | 352 | ``` common-lisp 353 | CL-USER> (multiple-value-bind (priv-key pub-key) (ssh-keys:generate-key-pair :rsa) 354 | (ssh-keys:write-key-to-path priv-key #P"~/.ssh/my-priv-rsa-key") 355 | (ssh-keys:write-key-to-path pub-key #P"~/.ssh/my-pub-rsa-key.pub")) 356 | NIL 357 | ``` 358 | 359 | The following example generates DSA private/public key pairs. 360 | 361 | ``` common-lisp 362 | CL-USER> (ssh-keys:generate-key-pair :dsa) 363 | ``` 364 | 365 | This example shows how to generate Ed25519 private/public key pairs. 366 | 367 | ``` common-lisp 368 | CL-USER> (ssh-keys:generate-key-pair :ed25519) 369 | ``` 370 | 371 | ECDSA keys can be generated using NIST P-256, NIST P-384 or NIST P-521 372 | curves. The following examples show how to create 256, 384 and 521 bit 373 | ECDSA keys. 374 | 375 | ``` common-lisp 376 | CL-USER> (ssh-keys:generate-key-pair :ecdsa-nistp256) 377 | CL-USER> (ssh-keys:generate-key-pair :ecdsa-nistp384) 378 | CL-USER> (ssh-keys:generate-key-pair :ecdsa-nistp521) 379 | ``` 380 | 381 | ## Tests 382 | 383 | Tests are provided as part of the `cl-ssh-keys.test` system. 384 | 385 | The following Common Lisp implementations have been tested and are 386 | known to work. 387 | 388 | * [SBCL](http://www.sbcl.org) 389 | * [CCL](https://ccl.clozure.com) 390 | 391 | In order to run the tests you can evaluate the following expressions. 392 | 393 | ``` common-lisp 394 | CL-USER> (ql:quickload :cl-ssh-keys.test) 395 | CL-USER> (asdf:test-system :cl-ssh-keys.test) 396 | ``` 397 | 398 | Or you can run the tests in a Docker container instead. 399 | 400 | First, build the Docker image. 401 | 402 | ``` shell 403 | docker build -t cl-ssh-keys . 404 | ``` 405 | 406 | Run the tests. 407 | 408 | ``` shell 409 | docker run --rm cl-ssh-keys 410 | ``` 411 | 412 | ## Contributing 413 | 414 | `cl-ssh-keys` is hosted on [Github][cl-ssh-keys]. Please contribute by 415 | reporting issues, suggesting features or by sending patches using pull 416 | requests. 417 | 418 | ## Authors 419 | 420 | * Marin Atanasov Nikolov (dnaeon@gmail.com) 421 | 422 | ## License 423 | 424 | This project is Open Source and licensed under the [BSD 425 | License][BSD License]. 426 | 427 | [RFC 4253]: https://tools.ietf.org/html/rfc4253 428 | [PROTOCOL.key]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD 429 | [Quicklisp]: https://www.quicklisp.org/beta/ 430 | [Quicklisp FAQ]: https://www.quicklisp.org/beta/faq.html 431 | [cl-ssh-keys]: https://github.com/dnaeon/cl-ssh-keys 432 | [BSD License]: http://opensource.org/licenses/BSD-2-Clause 433 | -------------------------------------------------------------------------------- /src/private-key.lisp: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2020 Marin Atanasov Nikolov 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer 10 | ;; in this position and unchanged. 11 | ;; 2. Redistributions in binary form must reproduce the above copyright 12 | ;; notice, this list of conditions and the following disclaimer in the 13 | ;; documentation and/or other materials provided with the distribution. 14 | ;; 15 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | ;; IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | (in-package :cl-ssh-keys) 27 | 28 | (defparameter *supported-kdf-names* 29 | '("none" "bcrypt") 30 | "Known and supported KDF names") 31 | 32 | (defparameter *default-kdf-rounds* 33 | 16 34 | "Default number of iterations to use when deriving a key") 35 | 36 | (defconstant +kdf-salt-size+ 16 37 | "Salt size in bytes") 38 | 39 | (alexandria:define-constant +private-key-auth-magic+ 40 | "openssh-key-v1" 41 | :test #'equal 42 | :documentation "OpenSSH private key AUTH_MAGIC header") 43 | 44 | (alexandria:define-constant +private-key-mark-begin+ 45 | "-----BEGIN OPENSSH PRIVATE KEY-----" 46 | :test #'string-equal 47 | :documentation "Beginning marker for OpenSSH private keys") 48 | 49 | (alexandria:define-constant +private-key-mark-end+ 50 | "-----END OPENSSH PRIVATE KEY-----" 51 | :test #'string-equal 52 | :documentation "Ending marker for OpenSSH private keys") 53 | 54 | (defclass base-private-key (base-key) 55 | ((public-key 56 | :initarg :public-key 57 | :initform (error "Must specify public key") 58 | :reader embedded-public-key 59 | :documentation "Public key embedded in the private key") 60 | (cipher-name 61 | :initarg :cipher-name 62 | :initform (error "Must specify cipher name") 63 | :accessor key-cipher-name 64 | :documentation "Private key cipher name") 65 | (kdf-name 66 | :initarg :kdf-name 67 | :initform (error "Must specify KDF name") 68 | :accessor key-kdf-name 69 | :documentation "Private key KDF name") 70 | (kdf-salt 71 | :initarg :kdf-salt 72 | :initform (ironclad:random-data +kdf-salt-size+) 73 | :reader key-kdf-salt 74 | :documentation "Salt used by the KDF function") 75 | (kdf-rounds 76 | :initarg :kdf-rounds 77 | :initform *default-kdf-rounds* 78 | :accessor key-kdf-rounds 79 | :documentation "Number of iterations used to derive the key") 80 | (checksum-int 81 | :initarg :checksum-int 82 | :initform (error "Must specify checksum integer") 83 | :reader key-checksum-int 84 | :documentation "Checksum integer for private keys") 85 | (passphrase 86 | :initarg :passphrase 87 | :initform nil 88 | :accessor key-passphrase 89 | :documentation "Passphrase used to encrypt the private key")) 90 | (:documentation "Base class for representing an OpenSSH private key")) 91 | 92 | (defmethod (setf key-kdf-name) :before (new-value (key base-private-key)) 93 | "Set KDF name for the private key" 94 | (unless (member new-value *supported-kdf-names* :test #'string=) 95 | (error 'base-error :description "Invalid KDF name"))) 96 | 97 | (defmethod (setf key-cipher-name) :before (new-value (key base-private-key)) 98 | "Set cipher name to use for encryption of the private key" 99 | (unless (member new-value (get-all-cipher-names) :test #'string=) 100 | (error 'base-error :description "Invalid cipher name"))) 101 | 102 | (defmethod (setf key-passphrase) :before (new-value (key base-private-key)) 103 | "Reset or remove passphrase for the private key. 104 | If NIL is provided then encryption will be removed for the private key." 105 | (etypecase new-value 106 | ;; Remove passphrase from the key, if passphrase is nil 107 | (null 108 | (setf (key-cipher-name key) "none") 109 | (setf (key-kdf-name key) "none")) 110 | ;; Setup cipher name and KDF name, if not already 111 | ;; set when changing passphrase. 112 | (string 113 | (with-accessors ((cipher key-cipher-name) (kdf key-kdf-name)) key 114 | (when (string= cipher "none") 115 | (setf cipher *default-cipher-name*)) 116 | (when (string= kdf "none") 117 | (setf kdf "bcrypt")))))) 118 | 119 | (defclass base-ecdsa-nistp-private-key (base-ecdsa-nistp-key base-private-key) 120 | () 121 | (:documentation "Base class for representing an OpenSSH ECDSA private key")) 122 | 123 | (defmethod rfc4251:decode ((type (eql :private-key)) stream &key passphrase) 124 | "Decodes an OpenSSH private key from the given stream" 125 | (let* (cipher 126 | kdf-name 127 | kdf-options 128 | kdf-options-stream 129 | salt 130 | rounds 131 | pub-key-stream 132 | public-key 133 | check-int-1 134 | check-int-2 135 | encrypted-buffer 136 | encrypted-stream 137 | args 138 | priv-key 139 | comment 140 | key-is-encrypted-p 141 | (total 0)) 142 | ;; Parse AUTH_MAGIC header 143 | (multiple-value-bind (auth-magic size) (rfc4251:decode :c-string stream) 144 | (incf total size) 145 | (unless (string= auth-magic +private-key-auth-magic+) 146 | (error 'invalid-key-error 147 | :description "Expected AUTH_MAGIC header not found"))) 148 | 149 | ;; Parse cipher name 150 | (multiple-value-bind (cipher-name size) (rfc4251:decode :string stream) 151 | (incf total size) 152 | (setf cipher (get-cipher-by-name-or-lose cipher-name))) 153 | 154 | (unless (string= (getf cipher :name) "none") 155 | (setf key-is-encrypted-p t)) 156 | 157 | ;; Parse KDF name 158 | (multiple-value-bind (value size) (rfc4251:decode :string stream) 159 | (incf total size) 160 | (setf kdf-name value) 161 | ;; KDF name can be either "none" or "bcrypt" 162 | (unless (or (string= kdf-name "none") (string= kdf-name "bcrypt")) 163 | (error 'invalid-key-error 164 | :description (format nil "Unknown KDF function name ~a" value)))) 165 | 166 | ;; Parse kdf options 167 | (multiple-value-bind (value size) (rfc4251:decode :buffer stream) 168 | (incf total size) 169 | (setf kdf-options value)) 170 | 171 | ;; KDF options will contain the salt and the number of rounds, if 172 | ;; the key has been encrypted using a known cipher. 173 | (when key-is-encrypted-p 174 | (setf kdf-options-stream (rfc4251:make-binary-input-stream kdf-options)) 175 | (setf salt (rfc4251:decode :buffer kdf-options-stream)) 176 | (setf rounds (rfc4251:decode :uint32 kdf-options-stream))) 177 | 178 | ;; Parse number of keys, which are embedded in the private key. 179 | ;; Only 1 key is expected here. 180 | (multiple-value-bind (value size) (rfc4251:decode :uint32 stream) 181 | (incf total size) 182 | (unless (= value 1) 183 | (error 'invalid-key-error 184 | :description "Expected only one key embedded in the blob"))) 185 | 186 | ;; Parse public key section. 187 | ;; We need to decode the buffer first and then decode the embedded key. 188 | (multiple-value-bind (buffer size) (rfc4251:decode :buffer stream) 189 | (incf total size) 190 | (setf pub-key-stream (rfc4251:make-binary-input-stream buffer)) 191 | (setf public-key (rfc4251:decode :public-key pub-key-stream))) 192 | 193 | ;; Read encrypted section. 194 | (multiple-value-bind (buffer size) (rfc4251:decode :buffer stream) 195 | (incf total size) 196 | (setf encrypted-buffer buffer)) 197 | 198 | ;; Check size of encrypted data against the cipher blocksize that was used 199 | (unless (zerop (mod (length encrypted-buffer) (getf cipher :blocksize))) 200 | (error 'invalid-key-error 201 | :description "Invalid private key format")) 202 | 203 | ;; Decrypt in-place, if needed. 204 | (when key-is-encrypted-p 205 | (unless passphrase 206 | (error 'base-error :description "Key is encrypted. Passphrase is required")) 207 | (decrypt-private-key encrypted-buffer 208 | (getf cipher :name) 209 | (ironclad:ascii-string-to-byte-array passphrase) 210 | salt 211 | rounds)) 212 | 213 | ;; Continue decoding the rest of the encrypted section, which by now should 214 | ;; be decrypted, if given a correct passphrase. 215 | (setf encrypted-stream (rfc4251:make-binary-input-stream encrypted-buffer)) 216 | 217 | ;; Decode checksum integers. 218 | ;; If they don't match this means that the private key was 219 | ;; not successfully decrypted. 220 | (setf check-int-1 (rfc4251:decode :uint32 encrypted-stream)) 221 | (setf check-int-2 (rfc4251:decode :uint32 encrypted-stream)) 222 | (unless (= check-int-1 check-int-2) 223 | (error 'base-error 224 | :description "Checksum integers mismatch. Wrong passphrase.")) 225 | 226 | ;; Parse key type name. Must match with the one of the public key. 227 | (unless (string= (rfc4251:decode :string encrypted-stream) 228 | (getf (key-kind public-key) :plain-name)) 229 | (error 'invalid-key-error 230 | :description "Private and public key types mismatch")) 231 | 232 | ;; Dispatch to the respective private key implementation for 233 | ;; decoding the rest of the encrypted stream 234 | (setf args (list :kind (key-kind public-key) 235 | :public-key public-key 236 | :cipher-name (getf cipher :name) 237 | :kdf-name kdf-name 238 | :kdf-salt salt 239 | :kdf-rounds rounds 240 | :passphrase passphrase 241 | :checksum-int check-int-1)) 242 | 243 | (setf priv-key 244 | (case (getf (key-kind public-key) :id) ;; Dispatch based on the public key id 245 | (:ssh-rsa (apply #'rfc4251:decode :rsa-private-key encrypted-stream args)) 246 | (:ssh-dss (apply #'rfc4251:decode :dsa-private-key encrypted-stream args)) 247 | (:ssh-ed25519 (apply #'rfc4251:decode :ed25519-private-key encrypted-stream args)) 248 | (:ecdsa-sha2-nistp256 (apply #'rfc4251:decode :ecdsa-nistp256-private-key encrypted-stream args)) 249 | (:ecdsa-sha2-nistp384 (apply #'rfc4251:decode :ecdsa-nistp384-private-key encrypted-stream args)) 250 | (:ecdsa-sha2-nistp521 (apply #'rfc4251:decode :ecdsa-nistp521-private-key encrypted-stream args)) 251 | (t 252 | (error 'invalid-key-error 253 | :description "Invalid or unknown private key")))) 254 | 255 | ;; Decode comment 256 | (setf comment (rfc4251:decode :string encrypted-stream)) 257 | (setf (key-comment priv-key) comment) 258 | 259 | ;; Also set the comment on the embedded public key 260 | (setf (key-comment public-key) comment) 261 | 262 | ;; Perform a deterministic pad check 263 | (unless (private-key-padding-is-correct-p encrypted-stream) 264 | (error 'invalid-key-error 265 | :description "Invalid private key padding")) 266 | 267 | (values priv-key total))) 268 | 269 | (defmethod rfc4251:encode ((type (eql :private-key)) (key base-private-key) stream &key) 270 | "Encodes the private key in OpenSSH private key format" 271 | (let* ((tmp-stream (rfc4251:make-binary-output-stream)) ;; Temporary buffer to which we write 272 | (pub-key-stream (rfc4251:make-binary-output-stream)) 273 | (kdf-stream (rfc4251:make-binary-output-stream)) 274 | (encrypted-stream (rfc4251:make-binary-output-stream)) 275 | (key-type (key-kind key)) 276 | (key-type-plain (getf key-type :plain-name)) 277 | (key-id (getf key-type :id)) 278 | (cipher (get-cipher-by-name-or-lose (key-cipher-name key))) 279 | (cipher-blocksize (getf cipher :blocksize)) 280 | encrypted-buffer) 281 | (unless cipher 282 | (error 'invalid-key-error 283 | :description "Invalid cipher name")) 284 | 285 | (rfc4251:encode :c-string +private-key-auth-magic+ tmp-stream) ;; AUTH_MAGIC header 286 | (rfc4251:encode :string (key-cipher-name key) tmp-stream) ;; Cipher name 287 | 288 | ;; KDF name and options. 289 | (rfc4251:encode :string (key-kdf-name key) tmp-stream) ;; KDF name 290 | 291 | ;; The salt and rounds reside in a separate buffer, 292 | ;; when the key is encoded. Encode salt and rounds, only 293 | ;; if we are encrypting the private key with a passphrase. 294 | (when (key-passphrase key) 295 | (rfc4251:encode :buffer (key-kdf-salt key) kdf-stream) ;; KDF salt 296 | (rfc4251:encode :uint32 (key-kdf-rounds key) kdf-stream)) ;; KDF rounds 297 | 298 | ;; Write out the KDF options buffer to the temp stream 299 | (rfc4251:encode :buffer 300 | (rfc4251:get-binary-stream-bytes kdf-stream) 301 | tmp-stream) 302 | 303 | (rfc4251:encode :uint32 #x01 tmp-stream) ;; Number of keys 304 | 305 | ;; Public key buffer 306 | (rfc4251:encode :public-key 307 | (embedded-public-key key) 308 | pub-key-stream) 309 | (rfc4251:encode :buffer 310 | (rfc4251:get-binary-stream-bytes pub-key-stream) 311 | tmp-stream) 312 | 313 | ;; Encrypted buffer 314 | (rfc4251:encode :uint32 (key-checksum-int key) encrypted-stream) ;; checkint 1 315 | (rfc4251:encode :uint32 (key-checksum-int key) encrypted-stream) ;; checkint 2 316 | (rfc4251:encode :string key-type-plain encrypted-stream) ;; key type name 317 | 318 | ;; Dispatch further encoding to the respective implementation 319 | (case key-id 320 | (:ssh-rsa (rfc4251:encode :rsa-private-key key encrypted-stream)) 321 | (:ssh-dss (rfc4251:encode :dsa-private-key key encrypted-stream)) 322 | (:ssh-ed25519 (rfc4251:encode :ed25519-private-key key encrypted-stream)) 323 | (:ecdsa-sha2-nistp256 (rfc4251:encode :ecdsa-nistp256-private-key key encrypted-stream)) 324 | (:ecdsa-sha2-nistp384 (rfc4251:encode :ecdsa-nistp384-private-key key encrypted-stream)) 325 | (:ecdsa-sha2-nistp521 (rfc4251:encode :ecdsa-nistp521-private-key key encrypted-stream)) 326 | (t 327 | (error 'unsupported-key-error 328 | :description (format nil "Unsupported private key type ~a" key-type-plain)))) 329 | 330 | ;; Comment 331 | (rfc4251:encode :string (or (key-comment key) "") encrypted-stream) 332 | 333 | ;; Padding 334 | (loop for size = (length (rfc4251:get-binary-stream-bytes encrypted-stream)) 335 | for i from 1 336 | until (zerop (mod size cipher-blocksize)) 337 | do 338 | (rfc4251:encode :byte i encrypted-stream)) 339 | 340 | ;; Write out the encrypted buffer, and optionally encrypt if we have a passphrase 341 | (setf encrypted-buffer (rfc4251:get-binary-stream-bytes encrypted-stream)) 342 | 343 | (when (key-passphrase key) 344 | (encrypt-private-key encrypted-buffer 345 | (key-cipher-name key) 346 | (ironclad:ascii-string-to-byte-array (key-passphrase key)) 347 | (key-kdf-salt key) 348 | (key-kdf-rounds key))) 349 | 350 | (rfc4251:encode :buffer encrypted-buffer tmp-stream) 351 | 352 | ;; Flush out the temp buffer 353 | (rfc4251:encode :raw-bytes (rfc4251:get-binary-stream-bytes tmp-stream) stream))) 354 | 355 | (defmethod fingerprint ((hash-spec (eql :md5)) (key base-private-key) &key) 356 | "Computes the MD5 fingerprint of the embedded public key" 357 | (with-slots (public-key) key 358 | (fingerprint :md5 public-key))) 359 | 360 | (defmethod fingerprint ((hash-spec (eql :sha1)) (key base-private-key) &key) 361 | "Computes the SHA-1 fingerprint of the embedded public key" 362 | (with-slots (public-key) key 363 | (fingerprint :sha1 public-key))) 364 | 365 | (defmethod fingerprint ((hash-spec (eql :sha256)) (key base-private-key) &key) 366 | "Computes the SHA-256 fingerprint of the embedded public key" 367 | (with-slots (public-key) key 368 | (fingerprint :sha256 public-key))) 369 | 370 | (defmethod write-key ((key base-private-key) &optional (stream *standard-output*)) 371 | "Writes the private key in its text representation" 372 | (let* ((s (rfc4251:make-binary-output-stream)) 373 | (size (rfc4251:encode :private-key key s)) 374 | (data (rfc4251:get-binary-stream-bytes s)) 375 | (encoded (cl-base64:usb8-array-to-base64-string data))) 376 | (declare (ignore size)) 377 | (format stream "~a~&" +private-key-mark-begin+) 378 | (loop for char across encoded 379 | for i from 1 do 380 | (when (zerop (mod (1- i) 70)) 381 | (format stream "~&")) 382 | (write-char char stream)) 383 | (format stream "~&~a~&" +private-key-mark-end+))) 384 | 385 | (defmacro with-private-key ((var text &key passphrase) &body body) 386 | "Parses a private key from the given TEXT and evaluates the 387 | BODY with VAR bound to the decoded private key" 388 | `(let ((,var (parse-private-key ,text :passphrase ,passphrase))) 389 | ,@body)) 390 | 391 | (defmacro with-private-key-file ((var path &key passphrase) &body body) 392 | "Parses a private key from the given PATH and evaluates the 393 | BODY with VAR bound to the decoded private key" 394 | `(let ((,var (parse-private-key-file ,path :passphrase ,passphrase))) 395 | ,@body)) 396 | 397 | (defun extract-private-key (stream) 398 | "Extracts the private key contents from the given stream" 399 | (with-output-to-string (s) 400 | ;; First line should be the beginning marker 401 | (unless (string= +private-key-mark-begin+ 402 | (read-line stream)) 403 | (error 'invalid-key-error 404 | :description "Invalid private key format")) 405 | ;; Read until the end marker 406 | (loop for line = (read-line stream nil nil) 407 | until (string= line +private-key-mark-end+) 408 | do 409 | (write-string line s)) 410 | s)) 411 | 412 | (defun extract-private-key-from-file (path) 413 | "Extracts the private key contents from the given path" 414 | (with-open-file (in path) 415 | (extract-private-key in))) 416 | 417 | (defun private-key-padding-is-correct-p (stream) 418 | "Predicate for deterministic check of padding after private key" 419 | (loop for byte = (read-byte stream nil :eof) 420 | for i from 1 421 | until (equal byte :eof) do 422 | (unless (= byte i) 423 | (return-from private-key-padding-is-correct-p nil))) 424 | t) 425 | 426 | (defun encrypt-private-key (text cipher-name passphrase salt rounds) 427 | (let ((cipher (get-cipher-for-encryption/decryption cipher-name passphrase salt rounds))) 428 | (ironclad:encrypt-in-place cipher text))) 429 | 430 | (defun decrypt-private-key (encrypted cipher-name passphrase salt rounds) 431 | (let ((cipher (get-cipher-for-encryption/decryption cipher-name passphrase salt rounds))) 432 | (ironclad:decrypt-in-place cipher encrypted))) 433 | 434 | (defun parse-private-key (text &key passphrase) 435 | "Parses an OpenSSH private key from the given plain-text string" 436 | (let* ((s (make-string-input-stream text)) 437 | (extracted (extract-private-key s)) 438 | (decoded (cl-base64:base64-string-to-usb8-array extracted)) 439 | (stream (rfc4251:make-binary-input-stream decoded))) 440 | (multiple-value-bind (key size) (rfc4251:decode :private-key stream :passphrase passphrase) 441 | (declare (ignore size)) 442 | key))) 443 | 444 | (defun parse-private-key-file (path &key passphrase) 445 | "Parses an OpenSSH private key from the given path" 446 | (parse-private-key (alexandria:read-file-into-string path) 447 | :passphrase passphrase)) 448 | -------------------------------------------------------------------------------- /src/cert-key.lisp: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2020-2021 Marin Atanasov Nikolov 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer 10 | ;; in this position and unchanged. 11 | ;; 2. Redistributions in binary form must reproduce the above copyright 12 | ;; notice, this list of conditions and the following disclaimer in the 13 | ;; documentation and/or other materials provided with the distribution. 14 | ;; 15 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | ;; IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | (in-package :cl-ssh-keys) 27 | 28 | (defconstant +ssh-cert-type-user+ 1 29 | "Indicates a user certificate") 30 | 31 | (defconstant +ssh-cert-type-host+ 2 32 | "Indicates a host certificate") 33 | 34 | (defconstant +ssh-cert-max-valid-to+ (1- (expt 2 64)) 35 | "Max expiry date for a certificate") 36 | 37 | (defparameter *ssh-cert-options* 38 | `((:name "force-command" 39 | :description ,(format nil "Specifies a command that is executed ~ 40 | (replacing any the user specified on the ~ 41 | ssh command-line) whenever this key is ~ 42 | used for authentication.") 43 | :is-critical t) 44 | (:name "source-address" 45 | :description ,(format nil "Comma-separated list of source addresses ~ 46 | from which this certificate is accepted ~ 47 | for authentication. Addresses are ~ 48 | specified in CIDR format (nn.nn.nn.nn/nn ~ 49 | or hhhh::hhhh/nn). ~ 50 | If this option is not present then ~ 51 | certificates may be presented from any ~ 52 | source address.") 53 | :is-critical t) 54 | (:name "no-presence-required" 55 | :description ,(format nil "Flag indicating that signatures made with ~ 56 | this certificate need not assert user ~ 57 | presence. This option only makes sense for ~ 58 | the U2F/FIDO security key types that support ~ 59 | this feature in their signature formats.") 60 | :is-critical nil) 61 | (:name "permit-X11-forwarding" 62 | :description ,(format nil "Flag indicating that X11 forwarding ~ 63 | should be permitted. X11 forwarding will ~ 64 | be refused if this option is absent.") 65 | :is-critical nil) 66 | (:name "permit-agent-forwarding" 67 | :description ,(format nil "Flag indicating that agent forwarding should be ~ 68 | allowed. Agent forwarding must not be permitted ~ 69 | unless this option is present.") 70 | :is-critical nil) 71 | (:name "permit-port-forwarding" 72 | :description ,(format nil "Flag indicating that port-forwarding ~ 73 | should be allowed. If this option is ~ 74 | not present then no port forwarding will ~ 75 | be allowed.") 76 | :is-critical nil) 77 | (:name "permit-pty" 78 | :description ,(format nil "Flag indicating that PTY allocation ~ 79 | should be permitted. In the absence of ~ 80 | this option PTY allocation will be disabled.") 81 | :is-critical nil) 82 | (:name "permit-user-rc" 83 | :description ,(format nil "Flag indicating that execution of ~ 84 | ~~/.ssh/rc should be permitted. Execution ~ 85 | of this script will not be permitted if ~ 86 | this option is not present.") 87 | :is-critical nil)) 88 | "Supported OpenSSH certificate options") 89 | 90 | (defun describe-cert-option (name) 91 | "Describe the OpenSSH certificate option with the given NAME" 92 | (let ((option (find name *ssh-cert-options* :key (lambda (x) (getf x :name)) :test #'string=))) 93 | (when option 94 | (format t "Name: ~a~%" (getf option :name)) 95 | (format t "Is Critical: ~a~%" (getf option :is-critical)) 96 | (format t "Description: ~a~%" (getf option :description))))) 97 | 98 | (defun get-supported-cert-options () 99 | "Returns a list of the supported certificate options" 100 | (mapcar (lambda (option) 101 | (getf option :name)) 102 | *ssh-cert-options*)) 103 | 104 | (defun get-cert-critical-options () 105 | "Returns the list of certificate critical options" 106 | (remove-if-not (lambda (item) 107 | (getf item :is-critical)) 108 | *ssh-cert-options*)) 109 | 110 | (defmethod rfc4251:decode ((type (eql :ssh-cert-valid-principals)) stream &key) 111 | "Decode the list of valid principals from an OpenSSH certificate key. 112 | 113 | The OpenSSH certificate format encodes the list of `valid principals` 114 | as a list of strings embedded within a buffer. While this seems okay 115 | it makes you wonder why not using the `name-list` data type from RFC 116 | 4251, section 5 instead, since `name-list` solves this particular 117 | problem." 118 | (let ((header-size 4) ;; uint32 specifying the buffer size 119 | (length (rfc4251:decode :uint32 stream))) ;; Number of bytes representing the buffer 120 | (when (zerop length) 121 | (return-from rfc4251:decode (values nil header-size))) 122 | (loop :for (value size) = (multiple-value-list (rfc4251:decode :string stream)) 123 | :summing size :into total 124 | :collect value :into result 125 | :while (< total length) 126 | :finally (return (values result (+ header-size total)))))) 127 | 128 | (defmethod rfc4251:encode ((type (eql :ssh-cert-valid-principals)) value stream &key) 129 | "Encode a list of valid principals into an OpenSSH certificate key" 130 | (let ((s (rfc4251:make-binary-output-stream))) 131 | (loop :for item :in value :do 132 | (rfc4251:encode :string item s)) 133 | (rfc4251:encode :buffer (rfc4251:get-binary-stream-bytes s) stream))) 134 | 135 | (defmethod rfc4251:decode ((type (eql :ssh-cert-critical-options)) stream &key) 136 | "Decode OpenSSH certificate critical options. 137 | 138 | Please refer to [1] for more details. 139 | 140 | [1]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys" 141 | (let ((header-size 4) ;; uint32 specifying the buffer size 142 | (length (rfc4251:decode :uint32 stream))) ;; Number of bytes representing the options data 143 | (when (zerop length) 144 | (return-from rfc4251:decode (values nil header-size))) 145 | (loop :for (name name-size) = (multiple-value-list (rfc4251:decode :string stream)) 146 | ;; The data is packed inside another string buffer 147 | :for (buffer buffer-size) = (multiple-value-list (rfc4251:decode :buffer stream)) 148 | :for data-stream = (rfc4251:make-binary-input-stream buffer) 149 | :for data = (rfc4251:decode :string data-stream) 150 | :summing name-size :into total 151 | :summing buffer-size :into total 152 | :collect (cons name data) :into result 153 | :while (< total length) 154 | :finally (return (values result (+ header-size total)))))) 155 | 156 | (defmethod rfc4251:encode ((type (eql :ssh-cert-critical-options)) value stream &key) 157 | "Encode OpenSSH certificate critical options list. 158 | VALUE is a list a of cons cells, each representing a 159 | critical option, e.g. (OPTION-NAME . OPTION-VALUE)." 160 | (let ((s (rfc4251:make-binary-output-stream))) ;; Use a temp stream and encode it as a whole once ready 161 | (loop :for (option-name . option-value) :in value :do 162 | (rfc4251:encode :string option-name s) 163 | ;; The option-value is packed inside a buffer 164 | (rfc4251:with-binary-output-stream (option-s) 165 | (rfc4251:encode :string option-value option-s) 166 | (rfc4251:encode :buffer (rfc4251:get-binary-stream-bytes option-s) s))) 167 | (rfc4251:encode :buffer (rfc4251:get-binary-stream-bytes s) stream))) 168 | 169 | (defmethod rfc4251:decode ((type (eql :ssh-cert-extensions)) stream &key) 170 | "Decode OpenSSH certificate extensions. 171 | 172 | Please refer to [1] for more details. 173 | 174 | [1]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys" 175 | (let ((header-size 4) ;; uint32 specifying the buffer size 176 | (length (rfc4251:decode :uint32 stream))) ;; Number of bytes representing the options data 177 | (when (zerop length) 178 | (return-from rfc4251:decode (values nil header-size))) 179 | (loop :for (name name-size) = (multiple-value-list (rfc4251:decode :string stream)) 180 | :for (nil data-size) = (multiple-value-list (rfc4251:decode :string stream)) 181 | :summing name-size :into total 182 | :summing data-size :into total 183 | :collect name :into result 184 | :while (< total length) 185 | :finally (return (values result (+ header-size total)))))) 186 | 187 | (defmethod rfc4251:encode ((type (eql :ssh-cert-extensions)) value stream &key) 188 | "Encodes a list of OpenSSH certificate extensions 189 | 190 | Please refer to [1] for more details. 191 | 192 | [1]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys" 193 | (let ((s (rfc4251:make-binary-output-stream))) 194 | (loop :for name :in value :do 195 | (rfc4251:encode :string name s) 196 | ;; Extensions are flags only, so no data is associated with them 197 | (rfc4251:encode :string "" s)) 198 | (rfc4251:encode :buffer (rfc4251:get-binary-stream-bytes s) stream))) 199 | 200 | (defclass certificate (base-public-key) 201 | ((nonce 202 | :initarg :nonce 203 | :initform (error "Must provide nonce") 204 | :accessor cert-nonce 205 | :documentation "CA-provided nonce") 206 | (key 207 | :initarg :key 208 | :initform (error "Must specify certificate public key") 209 | :reader cert-key 210 | :documentation "The public key of the user/host") 211 | (serial 212 | :initarg :serial 213 | :initform 0 214 | :accessor cert-serial 215 | :documentation "Optional certificate serial number set by the CA") 216 | (type 217 | :initarg :type 218 | :initform (error "Must specify certificate type") 219 | :accessor cert-type 220 | :documentation "Certificate type. Must be either +SSH-CERT-TYPE-USER+ or +SSH-CERT-TYPE-HOST+") 221 | (key-id 222 | :initarg :key-id 223 | :initform nil 224 | :accessor cert-key-id 225 | :documentation "Key identity filled in by the CA at the time of signing") 226 | (valid-principals 227 | :initarg :valid-principals 228 | :initform nil 229 | :accessor cert-valid-principals 230 | :documentation "List of usernames/hostnames for which this certificate is valid") 231 | (valid-after 232 | :initarg :valid-after 233 | :initform 0 234 | :accessor cert-valid-after 235 | :documentation "The validity period after which the certificate is valid") 236 | (valid-before 237 | :initarg :valid-before 238 | :initform +ssh-cert-max-valid-to+ 239 | :accessor cert-valid-before 240 | :documentation "The validity period before which the certificate is valid") 241 | (critical-options 242 | :initarg :critical-options 243 | :initform nil 244 | :accessor cert-critical-options 245 | :documentation "Certificate critical options") 246 | (extensions 247 | :initarg :extensions 248 | :initform nil 249 | :accessor cert-extensions 250 | :documentation "Certificate extensions") 251 | (reserved 252 | :initform nil 253 | :initarg :reserved 254 | :reader cert-reserved 255 | :documentation "Currently unused and ignored in this version of the protocol") 256 | (signature-key 257 | :initarg :signature-key 258 | :initform (error "Must specify signature key") 259 | :accessor cert-signature-key 260 | :documentation "The public key of the CA that signed the certificate") 261 | (signature 262 | :initarg :signature 263 | :accessor cert-signature 264 | :initform (error "Must specify signature") 265 | :documentation "The certificate signature")) 266 | (:documentation "An OpenSSH certificate key")) 267 | 268 | (defmethod get-bytes-for-signing ((cert certificate) &key) 269 | "Returns the portion of the certificate key which will be signed. 270 | The bytes for signing represent everything up to the signature." 271 | (rfc4251:with-binary-output-stream (s) 272 | (rfc4251:encode :string (getf (key-kind cert) :name) s) ;; Kind 273 | (rfc4251:encode :buffer (cert-nonce cert) s) ;; Nonce 274 | 275 | ;; Client public key. Note that for certificates the public key is 276 | ;; not preceeded by the key kind string. 277 | (rfc4251:encode :public-key (cert-key cert) s :encode-key-type-p nil) 278 | (rfc4251:encode :uint64 (cert-serial cert) s) ;; Serial 279 | (rfc4251:encode :uint32 (cert-type cert) s) ;; Cert type (user or host) 280 | (rfc4251:encode :string (cert-key-id cert) s) ;; Key identity 281 | (rfc4251:encode :ssh-cert-valid-principals (cert-valid-principals cert) s) ;; Valid principals 282 | (rfc4251:encode :uint64 (cert-valid-after cert) s) ;; Valid after 283 | (rfc4251:encode :uint64 (cert-valid-before cert) s) ;; Valid before 284 | (rfc4251:encode :ssh-cert-critical-options (cert-critical-options cert) s) ;; Critical options 285 | (rfc4251:encode :ssh-cert-extensions (cert-extensions cert) s) ;; Extensions 286 | (rfc4251:encode :buffer (cert-reserved cert) s) ;; Reserved 287 | 288 | ;; Signature key. This one resides in a buffer of it's own 289 | (rfc4251:with-binary-output-stream (signature-key-s) 290 | (rfc4251:encode :public-key (cert-signature-key cert) signature-key-s) 291 | (rfc4251:encode :buffer (rfc4251:get-binary-stream-bytes signature-key-s) s)) 292 | (rfc4251:get-binary-stream-bytes s))) 293 | 294 | (defmethod rfc4251:encode ((type (eql :ssh-cert-key)) (cert certificate) stream &key) 295 | "Encodes the OpenSSH certificate key into the given binary stream" 296 | (rfc4251:encode :buffer (cert-nonce cert) stream) ;; Nonce 297 | 298 | ;; Client public key. Note that for certificates the public key is 299 | ;; not preceeded by the key kind string. 300 | (rfc4251:encode :public-key (cert-key cert) stream :encode-key-type-p nil) 301 | (rfc4251:encode :uint64 (cert-serial cert) stream) ;; Serial 302 | (rfc4251:encode :uint32 (cert-type cert) stream) ;; Cert type (user or host) 303 | (rfc4251:encode :string (cert-key-id cert) stream) ;; Key identity 304 | (rfc4251:encode :ssh-cert-valid-principals (cert-valid-principals cert) stream) ;; Valid principals 305 | (rfc4251:encode :uint64 (cert-valid-after cert) stream) ;; Valid after 306 | (rfc4251:encode :uint64 (cert-valid-before cert) stream) ;; Valid before 307 | (rfc4251:encode :ssh-cert-critical-options (cert-critical-options cert) stream) ;; Critical options 308 | (rfc4251:encode :ssh-cert-extensions (cert-extensions cert) stream) ;; Extensions 309 | (rfc4251:encode :buffer (cert-reserved cert) stream) ;; Reserved 310 | 311 | ;; Signature key. This one resides in a buffer of it's own 312 | (rfc4251:with-binary-output-stream (signature-key-s) 313 | (rfc4251:encode :public-key (cert-signature-key cert) signature-key-s) 314 | (rfc4251:encode :buffer (rfc4251:get-binary-stream-bytes signature-key-s) stream)) 315 | 316 | ;; Signature. This one resides in a separate buffer as well. 317 | (rfc4251:with-binary-output-stream (signature-s) 318 | (rfc4251:encode :cert-signature (cert-signature cert) signature-s) 319 | (rfc4251:encode :buffer (rfc4251:get-binary-stream-bytes signature-s) stream))) 320 | 321 | (defmethod rfc4251:decode ((type (eql :ssh-cert-key)) stream &key kind comment) 322 | "Decodes an OpenSSH certificate key from the given stream" 323 | (let ((client-pk-plain-name (getf kind :plain-name)) 324 | (total 0) ;; Total bytes read from the stream 325 | nonce 326 | client-pk ;; Client public key 327 | serial ;; Certificate serial number 328 | cert-key-type ;; Cert key type (user or host key) 329 | key-identity 330 | valid-principals 331 | valid-after 332 | valid-before 333 | critical-options 334 | extensions 335 | reserved 336 | signature-key 337 | signature 338 | cert) 339 | ;; Nonce 340 | (multiple-value-bind (value size) (rfc4251:decode :buffer stream) 341 | ;; nonce should be 16 or 32 bytes in size 342 | (let ((size (length value))) 343 | (unless (or (= 16 size) (= 32 size)) 344 | (error 'invalid-key-error 345 | :description "nonce should be 16 or 32 bytes"))) 346 | (incf total size) 347 | (setf nonce value)) 348 | 349 | ;; Client public key 350 | (multiple-value-bind (value size) 351 | (rfc4251:decode :public-key stream :key-type-name client-pk-plain-name :comment comment) 352 | (incf total size) 353 | (setf client-pk value)) 354 | 355 | ;; Serial 356 | (multiple-value-bind (value size) (rfc4251:decode :uint64 stream) 357 | (incf total size) 358 | (setf serial value)) 359 | 360 | ;; Cert key type (user or host) 361 | (multiple-value-bind (value size) (rfc4251:decode :uint32 stream) 362 | (unless (member value (list +ssh-cert-type-user+ +ssh-cert-type-host+)) 363 | (error 'invalid-key-error 364 | :description "invalid cert key type")) 365 | (incf total size) 366 | (setf cert-key-type value)) 367 | 368 | ;; Cert key identity 369 | (multiple-value-bind (value size) (rfc4251:decode :string stream) 370 | (incf total size) 371 | (setf key-identity value)) 372 | 373 | ;; Valid principals 374 | (multiple-value-bind (value size) (rfc4251:decode :ssh-cert-valid-principals stream) 375 | (incf total size) 376 | (setf valid-principals value)) 377 | 378 | ;; Valid after 379 | (multiple-value-bind (value size) (rfc4251:decode :uint64 stream) 380 | (incf total size) 381 | (setf valid-after value)) 382 | 383 | ;; Valid before 384 | (multiple-value-bind (value size) (rfc4251:decode :uint64 stream) 385 | (incf total size) 386 | (setf valid-before value)) 387 | 388 | ;; Critical options 389 | (multiple-value-bind (value size) (rfc4251:decode :ssh-cert-critical-options stream) 390 | (incf total size) 391 | (setf critical-options value)) 392 | 393 | ;; Extensions 394 | (multiple-value-bind (value size) (rfc4251:decode :ssh-cert-extensions stream) 395 | (incf total size) 396 | (setf extensions value)) 397 | 398 | ;; Reserved 399 | (multiple-value-bind (value size) (rfc4251:decode :buffer stream) 400 | ;; Reserved field is currently unused and ignored 401 | (unless (zerop (length value)) 402 | (error 'invalid-key-error 403 | :description "invalid/unknown reserved field")) 404 | (incf total size) 405 | (setf reserved value)) 406 | 407 | ;; Signature key. This one resides in a buffer on it's own, so 408 | ;; decode the buffer first. 409 | (multiple-value-bind (value size) (rfc4251:decode :buffer stream) 410 | (incf total size) 411 | (cl-rfc4251:with-binary-input-stream (s value) 412 | (setf signature-key (rfc4251:decode :public-key s)))) 413 | 414 | ;; Signature. This one resides in a separate buffer as well. 415 | (multiple-value-bind (value size) (rfc4251:decode :buffer stream) 416 | (incf total size) 417 | (cl-rfc4251:with-binary-input-stream (s value) 418 | (setf signature (rfc4251:decode :cert-signature s)))) 419 | 420 | ;; Create the certificate key 421 | (setf cert 422 | (make-instance 'certificate 423 | :comment comment 424 | :kind kind 425 | :nonce nonce 426 | :key client-pk 427 | :serial serial 428 | :type cert-key-type 429 | :key-id key-identity 430 | :valid-principals valid-principals 431 | :valid-after valid-after 432 | :valid-before valid-before 433 | :critical-options critical-options 434 | :extensions extensions 435 | :reserved reserved 436 | :signature-key signature-key 437 | :signature signature)) 438 | 439 | (unless (verify-signature (cert-signature-key cert) 440 | (get-bytes-for-signing cert) 441 | signature) 442 | (error 'invalid-key-error 443 | :description "Signature verification failed")) 444 | 445 | (values cert total))) 446 | 447 | (defmethod fingerprint ((hash-spec (eql :md5)) (key certificate) &key) 448 | "Computes the MD5 fingerprint of the embedded client public key" 449 | (with-accessors ((public-key cert-key)) key 450 | (fingerprint :md5 public-key))) 451 | 452 | (defmethod fingerprint ((hash-spec (eql :sha1)) (key certificate) &key) 453 | "Computes the SHA1 fingerprint of the embedded client public key" 454 | (with-accessors ((public-key cert-key)) key 455 | (fingerprint :sha1 public-key))) 456 | 457 | (defmethod fingerprint ((hash-spec (eql :sha256)) (key certificate) &key) 458 | "Computes the SHA256 fingerprint of the embedded client public key" 459 | (with-accessors ((public-key cert-key)) key 460 | (fingerprint :sha256 public-key))) 461 | --------------------------------------------------------------------------------