├── .github └── workflows │ ├── ci.yml │ ├── release-notes.py │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── libsignify ├── Cargo.toml ├── examples │ ├── basic.rs │ ├── message.txt │ ├── message.txt.sig │ ├── test_key.pub │ └── test_key.sec ├── release.toml └── src │ ├── consts.rs │ ├── encoding.rs │ ├── errors.rs │ ├── key.rs │ ├── lib.rs │ └── test_utils.rs ├── release.toml └── signify ├── Cargo.toml ├── src └── main.rs └── tests ├── compare.sh ├── full-cycle.sh └── integration.sh /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: [main, staging, trying] 6 | 7 | env: 8 | CARGO_INCREMENTAL: 0 9 | CARGO_NET_RETRY: 10 10 | CI: 1 11 | RUST_BACKTRACE: short 12 | RUSTFLAGS: -D warnings 13 | RUSTUP_MAX_RETRIES: 10 14 | 15 | jobs: 16 | test: 17 | name: test 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | os: [ubuntu-latest, macos-latest, "windows-latest"] 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | with: 26 | fetch-depth: 0 27 | - uses: actions-rs/toolchain@v1 28 | with: 29 | toolchain: stable 30 | profile: minimal 31 | override: true 32 | - run: cargo test --all 33 | - run: cargo test --doc 34 | - run: cargo test --features std 35 | - run: make full-cycle 36 | - run: make integration 37 | - name: Test comparing against C signify 38 | if: matrix.os == 'ubuntu-latest' 39 | run: make compare 40 | 41 | check_fmt_and_docs: 42 | name: Checking fmt and docs 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v2 46 | with: 47 | fetch-depth: 0 48 | - uses: actions-rs/toolchain@v1 49 | with: 50 | toolchain: stable 51 | components: rustfmt, clippy 52 | override: true 53 | 54 | - name: fmt 55 | run: cargo fmt --all -- --check 56 | 57 | - name: Clippy 58 | run: cargo clippy --all --all-targets --all-features 59 | 60 | - name: Docs 61 | run: cargo doc 62 | 63 | no_std_build: 64 | name: "Ensure no_std can build" 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v2 68 | with: 69 | fetch-depth: 0 70 | - uses: actions-rs/toolchain@v1 71 | with: 72 | toolchain: stable 73 | profile: minimal 74 | override: true 75 | 76 | - name: Build 77 | run: | 78 | rustup target add thumbv6m-none-eabi 79 | cargo build -p libsignify --target thumbv6m-none-eabi 80 | -------------------------------------------------------------------------------- /.github/workflows/release-notes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import re 5 | import pathlib 6 | import sys 7 | 8 | 9 | _STDIO = pathlib.Path("-") 10 | 11 | 12 | def main(): 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument("-i", "--input", type=pathlib.Path, default="CHANGELOG.md") 15 | parser.add_argument("--tag", required=True) 16 | parser.add_argument("-o", "--output", type=pathlib.Path, required=True) 17 | args = parser.parse_args() 18 | 19 | if args.input == _STDIO: 20 | lines = sys.stdin.readlines() 21 | else: 22 | with args.input.open() as fh: 23 | lines = fh.readlines() 24 | version = args.tag.lstrip("v") 25 | 26 | note_lines = [] 27 | for line in lines: 28 | if line.startswith("# ") and version in line: 29 | note_lines.append(line) 30 | elif note_lines and line.startswith("# "): 31 | break 32 | elif note_lines: 33 | note_lines.append(line) 34 | 35 | notes = "".join(note_lines).strip() 36 | if args.output == _STDIO: 37 | print(notes) 38 | else: 39 | args.output.write_text(notes) 40 | 41 | 42 | if __name__ == "__main__": 43 | main() 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/starship/starship/blob/master/.github/workflows/deploy.yml 2 | 3 | name: Release 4 | on: 5 | push: 6 | tags: 7 | - "*" 8 | 9 | env: 10 | CRATE_NAME: signify 11 | 12 | jobs: 13 | # Build sources for every OS 14 | github_build: 15 | name: Build release binaries 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | target: 20 | - x86_64-unknown-linux-gnu 21 | - x86_64-unknown-linux-musl 22 | - x86_64-apple-darwin 23 | - x86_64-pc-windows-msvc 24 | include: 25 | - target: x86_64-unknown-linux-gnu 26 | os: ubuntu-latest 27 | name: x86_64-unknown-linux-gnu.tar.gz 28 | - target: x86_64-unknown-linux-musl 29 | os: ubuntu-latest 30 | name: x86_64-unknown-linux-musl.tar.gz 31 | - target: x86_64-apple-darwin 32 | os: macOS-latest 33 | name: x86_64-apple-darwin.tar.gz 34 | - target: x86_64-pc-windows-msvc 35 | os: windows-latest 36 | name: x86_64-pc-windows-msvc.zip 37 | runs-on: ${{ matrix.os }} 38 | steps: 39 | - name: Setup | Checkout 40 | uses: actions/checkout@v2 41 | 42 | # Cache files between builds 43 | - name: Setup | Cache Cargo 44 | uses: actions/cache@v2 45 | with: 46 | path: | 47 | ~/.cargo/registry 48 | ~/.cargo/git 49 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 50 | 51 | - name: Setup | Rust 52 | uses: actions-rs/toolchain@v1 53 | with: 54 | toolchain: stable 55 | override: true 56 | profile: minimal 57 | target: ${{ matrix.target }} 58 | 59 | - name: Setup | musl tools 60 | if: matrix.target == 'x86_64-unknown-linux-musl' 61 | run: sudo apt install -y musl-tools 62 | 63 | - name: Build | Build 64 | if: matrix.target != 'x86_64-unknown-linux-musl' 65 | run: cargo build --release --target ${{ matrix.target }} 66 | 67 | - name: Build | Build (musl) 68 | if: matrix.target == 'x86_64-unknown-linux-musl' 69 | run: cargo build --release --target ${{ matrix.target }} 70 | 71 | - name: Post Setup | Extract tag name 72 | shell: bash 73 | run: echo "##[set-output name=tag;]$(echo ${GITHUB_REF#refs/tags/})" 74 | id: extract_tag 75 | 76 | - name: Post Setup | Prepare artifacts [Windows] 77 | if: matrix.os == 'windows-latest' 78 | run: | 79 | mkdir target/stage 80 | cd target/${{ matrix.target }}/release 81 | strip ${{ env.CRATE_NAME }}.exe 82 | 7z a ../../stage/${{ env.CRATE_NAME }}-${{ steps.extract_tag.outputs.tag }}-${{ matrix.name }} ${{ env.CRATE_NAME }}.exe 83 | cd - 84 | 85 | - name: Post Setup | Prepare artifacts [-nix] 86 | if: matrix.os != 'windows-latest' 87 | run: | 88 | mkdir target/stage 89 | cd target/${{ matrix.target }}/release 90 | strip ${{ env.CRATE_NAME }} 91 | tar czvf ../../stage/${{ env.CRATE_NAME }}-${{ steps.extract_tag.outputs.tag }}-${{ matrix.name }} ${{ env.CRATE_NAME }} 92 | cd - 93 | 94 | - name: Post Setup | Upload artifacts 95 | uses: actions/upload-artifact@v2 96 | with: 97 | name: ${{ env.CRATE_NAME }}-${{ steps.extract_tag.outputs.tag }}-${{ matrix.name }} 98 | path: target/stage/* 99 | 100 | # Create GitHub release with Rust build targets and release notes 101 | github_release: 102 | name: Create GitHub Release 103 | needs: github_build 104 | runs-on: ubuntu-latest 105 | steps: 106 | - name: Setup | Checkout 107 | uses: actions/checkout@v2 108 | with: 109 | fetch-depth: 0 110 | 111 | - name: Extract tag name 112 | shell: bash 113 | run: echo "##[set-output name=tag;]$(echo ${GITHUB_REF#refs/tags/})" 114 | id: extract_tag 115 | 116 | - name: Setup | Artifacts 117 | uses: actions/download-artifact@v2 118 | 119 | - name: Setup | Release notes 120 | run: | 121 | ./.github/workflows/release-notes.py --tag ${{ steps.extract_tag.outputs.tag }} --output RELEASE.md 122 | cat RELEASE.md 123 | 124 | - name: Build | Publish 125 | uses: softprops/action-gh-release@v1 126 | with: 127 | files: ${{ env.CRATE_NAME }}-*/${{ env.CRATE_NAME }}-* 128 | body_path: RELEASE.md 129 | env: 130 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 131 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | signify/tests/signify 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Unreleased 2 | 3 | [Full changelog](https://github.com/badboy/signify-rs/compare/v0.6.0...main) 4 | 5 | # 0.6.0 (2023-12-01) 6 | 7 | [Full changelog](https://github.com/badboy/signify-rs/compare/v0.5.3...v0.6.0) 8 | 9 | # 0.5.3 (2022-06-04) 10 | 11 | [Full changelog](https://github.com/badboy/signify-rs/compare/v0.5.2...v0.5.3) 12 | 13 | # 0.5.2 (2022-02-06) 14 | 15 | [Full changelog](https://github.com/badboy/signify-rs/compare/v0.5.1...v0.5.2) 16 | 17 | # 0.5.1 (2022-02-06) 18 | 19 | [Full changelog](https://github.com/badboy/signify-rs/compare/v0.5.0...v0.5.1) 20 | 21 | * Unchanged re-release of 0.5.0 to fix the binary deploy 22 | 23 | # 0.5.0 (2022-02-06) 24 | 25 | [Full changelog](https://github.com/badboy/signify-rs/compare/v0.4.1...v0.5.0) 26 | 27 | * Split signify into a standalone library, available as `libsignify`, a `#![no_std]` library that implements the `signify` specification. 28 | Note: The library API is still unstable and might change in the next release. 29 | * Sweeping dependency updates and replacements, which also increase target compatibility. 30 | * Improved CLI usability 31 | * Complete rewrite of the crate's functionality. 32 | 33 | # v0.4.1 (2018-01-11) 34 | 35 | * Exclude temporary files from crate 36 | 37 | # v0.4.0 (2017-12-12) 38 | 39 | * Support embedded signatures 40 | * Auto-build binaries 41 | 42 | # v0.3.0 (2016-09-27) 43 | 44 | * Switched to *ring* for the crypto part 45 | * Cleaned up error handling 46 | * Prevent overwriting existing files 47 | * Check the keynum on keys 48 | * Ensure compatibility with original signify on CI 49 | 50 | # v0.2.0 (2016-06-27) 51 | 52 | Now with passphrase-protection for your secret key. 53 | 54 | # v0.1.0 (2016-06-14) 55 | 56 | The initial working release. 57 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anstyle" 7 | version = "1.0.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" 10 | 11 | [[package]] 12 | name = "base64ct" 13 | version = "1.6.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" 16 | 17 | [[package]] 18 | name = "bcrypt-pbkdf" 19 | version = "0.10.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" 22 | dependencies = [ 23 | "blowfish", 24 | "pbkdf2", 25 | "sha2", 26 | ] 27 | 28 | [[package]] 29 | name = "block-buffer" 30 | version = "0.10.4" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 33 | dependencies = [ 34 | "generic-array", 35 | ] 36 | 37 | [[package]] 38 | name = "blowfish" 39 | version = "0.9.1" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" 42 | dependencies = [ 43 | "byteorder", 44 | "cipher", 45 | ] 46 | 47 | [[package]] 48 | name = "byteorder" 49 | version = "1.4.3" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 52 | 53 | [[package]] 54 | name = "cfg-if" 55 | version = "1.0.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 58 | 59 | [[package]] 60 | name = "cipher" 61 | version = "0.4.4" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" 64 | dependencies = [ 65 | "crypto-common", 66 | "inout", 67 | ] 68 | 69 | [[package]] 70 | name = "clap" 71 | version = "4.4.6" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" 74 | dependencies = [ 75 | "clap_builder", 76 | "clap_derive", 77 | ] 78 | 79 | [[package]] 80 | name = "clap_builder" 81 | version = "4.4.6" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" 84 | dependencies = [ 85 | "anstyle", 86 | "clap_lex", 87 | ] 88 | 89 | [[package]] 90 | name = "clap_derive" 91 | version = "4.4.2" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" 94 | dependencies = [ 95 | "heck", 96 | "proc-macro2", 97 | "quote", 98 | "syn", 99 | ] 100 | 101 | [[package]] 102 | name = "clap_lex" 103 | version = "0.5.1" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" 106 | 107 | [[package]] 108 | name = "const-oid" 109 | version = "0.9.5" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" 112 | 113 | [[package]] 114 | name = "cpufeatures" 115 | version = "0.2.9" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" 118 | dependencies = [ 119 | "libc", 120 | ] 121 | 122 | [[package]] 123 | name = "crypto-common" 124 | version = "0.1.6" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 127 | dependencies = [ 128 | "generic-array", 129 | "typenum", 130 | ] 131 | 132 | [[package]] 133 | name = "curve25519-dalek" 134 | version = "4.1.3" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" 137 | dependencies = [ 138 | "cfg-if", 139 | "cpufeatures", 140 | "curve25519-dalek-derive", 141 | "digest", 142 | "fiat-crypto", 143 | "rustc_version", 144 | "subtle", 145 | "zeroize", 146 | ] 147 | 148 | [[package]] 149 | name = "curve25519-dalek-derive" 150 | version = "0.1.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" 153 | dependencies = [ 154 | "proc-macro2", 155 | "quote", 156 | "syn", 157 | ] 158 | 159 | [[package]] 160 | name = "der" 161 | version = "0.7.8" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" 164 | dependencies = [ 165 | "const-oid", 166 | "zeroize", 167 | ] 168 | 169 | [[package]] 170 | name = "digest" 171 | version = "0.10.7" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 174 | dependencies = [ 175 | "block-buffer", 176 | "crypto-common", 177 | "subtle", 178 | ] 179 | 180 | [[package]] 181 | name = "ed25519" 182 | version = "2.2.2" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "60f6d271ca33075c88028be6f04d502853d63a5ece419d269c15315d4fc1cf1d" 185 | dependencies = [ 186 | "pkcs8", 187 | "signature", 188 | ] 189 | 190 | [[package]] 191 | name = "ed25519-dalek" 192 | version = "2.0.0" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" 195 | dependencies = [ 196 | "curve25519-dalek", 197 | "ed25519", 198 | "rand_core", 199 | "serde", 200 | "sha2", 201 | "zeroize", 202 | ] 203 | 204 | [[package]] 205 | name = "fiat-crypto" 206 | version = "0.2.1" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" 209 | 210 | [[package]] 211 | name = "generic-array" 212 | version = "0.14.4" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" 215 | dependencies = [ 216 | "typenum", 217 | "version_check", 218 | ] 219 | 220 | [[package]] 221 | name = "getrandom" 222 | version = "0.2.10" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 225 | dependencies = [ 226 | "cfg-if", 227 | "libc", 228 | "wasi", 229 | ] 230 | 231 | [[package]] 232 | name = "heck" 233 | version = "0.4.1" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 236 | 237 | [[package]] 238 | name = "inout" 239 | version = "0.1.3" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" 242 | dependencies = [ 243 | "generic-array", 244 | ] 245 | 246 | [[package]] 247 | name = "libc" 248 | version = "0.2.148" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" 251 | 252 | [[package]] 253 | name = "libsignify" 254 | version = "0.6.0" 255 | dependencies = [ 256 | "base64ct", 257 | "bcrypt-pbkdf", 258 | "ed25519-dalek", 259 | "rand_core", 260 | "sha2", 261 | "static_assertions", 262 | "zeroize", 263 | ] 264 | 265 | [[package]] 266 | name = "pbkdf2" 267 | version = "0.12.2" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" 270 | dependencies = [ 271 | "digest", 272 | ] 273 | 274 | [[package]] 275 | name = "pkcs8" 276 | version = "0.10.2" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 279 | dependencies = [ 280 | "der", 281 | "spki", 282 | ] 283 | 284 | [[package]] 285 | name = "proc-macro2" 286 | version = "1.0.67" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" 289 | dependencies = [ 290 | "unicode-ident", 291 | ] 292 | 293 | [[package]] 294 | name = "quote" 295 | version = "1.0.33" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 298 | dependencies = [ 299 | "proc-macro2", 300 | ] 301 | 302 | [[package]] 303 | name = "rand_core" 304 | version = "0.6.4" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 307 | dependencies = [ 308 | "getrandom", 309 | ] 310 | 311 | [[package]] 312 | name = "rpassword" 313 | version = "7.0.0" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "26b763cb66df1c928432cc35053f8bd4cec3335d8559fc16010017d16b3c1680" 316 | dependencies = [ 317 | "libc", 318 | "winapi", 319 | ] 320 | 321 | [[package]] 322 | name = "rustc_version" 323 | version = "0.4.0" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 326 | dependencies = [ 327 | "semver", 328 | ] 329 | 330 | [[package]] 331 | name = "semver" 332 | version = "1.0.19" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" 335 | 336 | [[package]] 337 | name = "serde" 338 | version = "1.0.188" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" 341 | dependencies = [ 342 | "serde_derive", 343 | ] 344 | 345 | [[package]] 346 | name = "serde_derive" 347 | version = "1.0.188" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" 350 | dependencies = [ 351 | "proc-macro2", 352 | "quote", 353 | "syn", 354 | ] 355 | 356 | [[package]] 357 | name = "sha2" 358 | version = "0.10.8" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 361 | dependencies = [ 362 | "cfg-if", 363 | "cpufeatures", 364 | "digest", 365 | ] 366 | 367 | [[package]] 368 | name = "signature" 369 | version = "2.1.0" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" 372 | 373 | [[package]] 374 | name = "signify" 375 | version = "0.6.0" 376 | dependencies = [ 377 | "clap", 378 | "libsignify", 379 | "rand_core", 380 | "rpassword", 381 | ] 382 | 383 | [[package]] 384 | name = "spki" 385 | version = "0.7.2" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" 388 | dependencies = [ 389 | "base64ct", 390 | "der", 391 | ] 392 | 393 | [[package]] 394 | name = "static_assertions" 395 | version = "1.1.0" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 398 | 399 | [[package]] 400 | name = "subtle" 401 | version = "2.4.1" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" 404 | 405 | [[package]] 406 | name = "syn" 407 | version = "2.0.37" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" 410 | dependencies = [ 411 | "proc-macro2", 412 | "quote", 413 | "unicode-ident", 414 | ] 415 | 416 | [[package]] 417 | name = "typenum" 418 | version = "1.14.0" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" 421 | 422 | [[package]] 423 | name = "unicode-ident" 424 | version = "1.0.12" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 427 | 428 | [[package]] 429 | name = "version_check" 430 | version = "0.9.3" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 433 | 434 | [[package]] 435 | name = "wasi" 436 | version = "0.11.0+wasi-snapshot-preview1" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 439 | 440 | [[package]] 441 | name = "winapi" 442 | version = "0.3.9" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 445 | dependencies = [ 446 | "winapi-i686-pc-windows-gnu", 447 | "winapi-x86_64-pc-windows-gnu", 448 | ] 449 | 450 | [[package]] 451 | name = "winapi-i686-pc-windows-gnu" 452 | version = "0.4.0" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 455 | 456 | [[package]] 457 | name = "winapi-x86_64-pc-windows-gnu" 458 | version = "0.4.0" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 461 | 462 | [[package]] 463 | name = "zeroize" 464 | version = "1.6.0" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" 467 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "libsignify", 4 | "signify", 5 | ] 6 | 7 | resolver = "2" 8 | 9 | [profile.release] 10 | codegen-units = 1 11 | lto = true 12 | panic = "abort" 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Jan-Erik Rediger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: build cargo-test full-cycle integration 2 | 3 | build: 4 | cargo build $(BUILD_MODE) 5 | 6 | cargo-test: 7 | cargo test 8 | 9 | full-cycle: 10 | bash ./signify/tests/full-cycle.sh 11 | 12 | integration: 13 | bash ./signify/tests/integration.sh 14 | 15 | compare: 16 | bash ./signify/tests/compare.sh 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Signify - Ed25519 signature tool 2 | 3 | [![crates.io](https://img.shields.io/crates/v/signify.svg?style=flat-square)](https://crates.io/crates/signify) 4 | [![docs.rs docs](https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square)]([documentation]) 5 | [![License: MIT](https://img.shields.io/github/license/badboy/signify-rs?style=flat-square)](LICENSE) 6 | [![Build Status](https://img.shields.io/github/workflow/status/badboy/signify-rs/CI/main?style=flat-square)](https://github.com/badboy/signify-rs/actions/workflows/ci.yml) 7 | 8 | Create cryptographic signatures for files and verify them. 9 | This is based on [signify][], the OpenBSD tool to sign and verify signatures on files. 10 | It is based on the [Ed25519 public-key signature system][ed25519] by Bernstein et al. 11 | 12 | `signify-rs` is fully compatible with the original implementation. It can verify signatures generated by OpenBSD `signify` and signs data in a format that it can verify as well. 13 | 14 | You can read more about the ideas and concepts behind `signify` in [Securing OpenBSD From Us To You](https://www.openbsd.org/papers/bsdcan-signify.html). 15 | 16 | ## Installation 17 | 18 | ``` 19 | cargo install signify 20 | ``` 21 | 22 | ## Usage 23 | The CLI is designed to be compatible with the reference implementation and accepts the same command line flags as it. 24 | 25 | Create a key pair: 26 | 27 | ``` 28 | signify -G -p pubkey -s seckey 29 | ``` 30 | 31 | Sign a file using the secret key: 32 | 33 | ``` 34 | signify -S -s seckey -m README.md 35 | ``` 36 | 37 | Verify the signature: 38 | 39 | ``` 40 | signify -V -p pubkey -m README.md 41 | ``` 42 | 43 | To see how to use `libsignify`, check out the `examples/` directory or the [documentation]. 44 | 45 | ## Testing 46 | 47 | There are basic unit tests, but many more are needed for good coverage :disappointed:. 48 | 49 | However, we ensure that a full cycle of generating a keypair, then signing & verifying works. 50 | To run them, use the following commands: 51 | ```bash 52 | ./tests/full-cycle.sh 53 | ``` 54 | 55 | For correctness, we compare interoperability with the OpenBSD `signify`: 56 | ```bash 57 | ./tests/compare.sh 58 | ``` 59 | 60 | The complete test suite can be conveniently ran with `make test`. 61 | 62 | ## License 63 | 64 | MIT. See [LICENSE](LICENSE). 65 | 66 | [documentation]: https://docs.rs/libsignify 67 | [signify]: https://github.com/aperezdc/signify 68 | [ed25519]: https://ed25519.cr.yp.to/ 69 | -------------------------------------------------------------------------------- /libsignify/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libsignify" 3 | version = "0.6.0" 4 | authors = ["Jan-Erik Rediger ", "BlackHoleFox "] 5 | edition = "2021" 6 | 7 | keywords = ["cryptography", "security"] 8 | description = "Create cryptographic signatures for files and verify them" 9 | 10 | readme = "../README.md" 11 | license = "MIT" 12 | 13 | homepage = "https://github.com/badboy/signify-rs" 14 | repository = "https://github.com/badboy/signify-rs" 15 | 16 | [features] 17 | std = [] 18 | 19 | [dependencies] 20 | bcrypt-pbkdf = { version = "0.10", default-features = false } 21 | base64ct = { version = "1.6", default-features = false, features = ["alloc"] } 22 | ed25519-dalek = { version = "2", default-features = false, features = ["alloc", "fast", "rand_core"] } 23 | rand_core = { version = "0.6", default-features = false } 24 | sha2 = { version = "0.10", default-features = false } 25 | zeroize = { version = "1.4", default-features = false, features = ["alloc"] } 26 | 27 | [dev-dependencies] 28 | static_assertions = "1" 29 | -------------------------------------------------------------------------------- /libsignify/examples/basic.rs: -------------------------------------------------------------------------------- 1 | //! Basic example that shows how to verify a signature of some file. 2 | //! 3 | //! You could, for example, replace the file reading with a HTTP client. 4 | use libsignify::{Codeable, PublicKey, Signature}; 5 | use std::{fs, path::Path}; 6 | 7 | fn main() -> Result<(), Box> { 8 | println!("verifying signature of message file"); 9 | 10 | // Boilerplate so this code can run both via `cargo test --doc` and `cargo run --example`. 11 | // Not relevant to the example otherwise. 12 | let base_path = if std::env::var("CARGO_CRATE_NAME").is_ok() { 13 | Path::new("./examples/") 14 | } else { 15 | Path::new("./libsignify/examples/") 16 | }; 17 | 18 | // First, open the message to verify the validity of. 19 | let message = fs::read(base_path.join("message.txt"))?; 20 | 21 | // Then, get the public key of the signer. 22 | let (signer_id, _) = { 23 | let public_key_contents = fs::read_to_string(base_path.join("test_key.pub"))?; 24 | 25 | PublicKey::from_base64(&public_key_contents)? 26 | }; 27 | 28 | // Now, fetch the signature we have for the message. 29 | // 30 | // This could be from anywhere trusted, including a HTTP server for example. 31 | let (signature, _) = { 32 | let signature_contents = fs::read_to_string(base_path.join("message.txt.sig"))?; 33 | Signature::from_base64(&signature_contents)? 34 | }; 35 | 36 | // With all of the parts needed, the message can be checked now. 37 | match signer_id.verify(&message, &signature) { 38 | Ok(()) => { 39 | println!("message was verified!"); 40 | Ok(()) 41 | } 42 | Err(e) => { 43 | eprintln!("message did not verify: {}", e); 44 | Err(Box::new(e)) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /libsignify/examples/message.txt: -------------------------------------------------------------------------------- 1 | Was I tampered with? -------------------------------------------------------------------------------- /libsignify/examples/message.txt.sig: -------------------------------------------------------------------------------- 1 | untrusted comment: signature from signify secret key 2 | RWQEC93KpsGY1ra0DC0VQbBys7cJn8ql2GwQT++FPD3DikMSpW5neGMeQBi4cuJjeZJkJCHipOQ0R45RLRgGFu1pFZqyRBfTLQM= 3 | -------------------------------------------------------------------------------- /libsignify/examples/test_key.pub: -------------------------------------------------------------------------------- 1 | untrusted comment: signify-rs demo public key 2 | RWQEC93KpsGY1ru5XOWxiNxzzmA1qw3mNk5Kbg01y1BOyfcPQW0vOIQp 3 | -------------------------------------------------------------------------------- /libsignify/examples/test_key.sec: -------------------------------------------------------------------------------- 1 | untrusted comment: signify-rs demo secret key 2 | RWRCSwAAAAB5yy4ID9IWOh3gaWDT6zAj6Xi5hsLcWz0EC93KpsGY1g6BU//xXcmvE753Yos+Fsw7fSpzK14Pro9zhh9r2Bu+u7lc5bGI3HPOYDWrDeY2TkpuDTXLUE7J9w9BbS84hCk= 3 | -------------------------------------------------------------------------------- /libsignify/release.toml: -------------------------------------------------------------------------------- 1 | tag = false 2 | pre-release-replacements = [] 3 | -------------------------------------------------------------------------------- /libsignify/src/consts.rs: -------------------------------------------------------------------------------- 1 | //! Constants and type definitions from the `signify` design. 2 | 3 | use rand_core::RngCore; 4 | 5 | /// A number identifying a certain signing keypair. 6 | /// 7 | /// A short and easy to read [8 byte] digest of the key. 8 | /// 9 | /// [8 byte]: Self::LEN 10 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)] 11 | pub struct KeyNumber([u8; Self::LEN]); 12 | 13 | impl KeyNumber { 14 | /// The length of the key number, in bytes (8). 15 | pub const LEN: usize = 8; 16 | 17 | pub(crate) fn new(num: [u8; Self::LEN]) -> Self { 18 | Self(num) 19 | } 20 | 21 | pub(crate) fn generate(rng: &mut R) -> Self { 22 | let mut num = [0u8; Self::LEN]; 23 | rng.fill_bytes(&mut num); 24 | Self(num) 25 | } 26 | } 27 | 28 | impl AsRef<[u8]> for KeyNumber { 29 | fn as_ref(&self) -> &[u8] { 30 | &self.0 31 | } 32 | } 33 | 34 | pub(crate) const PUBLIC_KEY_LEN: usize = 32; 35 | pub(crate) const FULL_KEY_LEN: usize = 64; 36 | pub(crate) const SIG_LEN: usize = 64; 37 | 38 | pub(crate) const PKGALG: [u8; 2] = *b"Ed"; 39 | pub(crate) const KDFALG: [u8; 2] = *b"BK"; 40 | 41 | pub(crate) const COMMENT_HEADER: &str = "untrusted comment: "; 42 | pub(crate) const COMMENT_MAX_LEN: usize = 1024; 43 | 44 | /// The recommended number of KDF rounds to use when encrypting new secret keys. 45 | /// 46 | /// This value was selected to mirror the [OpenBSD implementation]'s choice. 47 | /// 48 | /// [OpenBSD implementation]: https://github.com/aperezdc/signify/blob/fa123eda2774c38abf98e43946baf604df85aea0/signify.c#L875 49 | pub const DEFAULT_KDF_ROUNDS: u32 = 42; 50 | 51 | #[cfg(test)] 52 | mod tests { 53 | use super::KeyNumber; 54 | use core::fmt::Debug; 55 | use core::hash::Hash; 56 | 57 | static_assertions::assert_impl_all!( 58 | KeyNumber: Clone, 59 | Copy, 60 | Debug, 61 | PartialEq, 62 | Eq, 63 | Hash, 64 | Ord, 65 | PartialOrd, 66 | Send, 67 | Sync 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /libsignify/src/encoding.rs: -------------------------------------------------------------------------------- 1 | use crate::consts::{ 2 | COMMENT_HEADER, COMMENT_MAX_LEN, FULL_KEY_LEN, PKGALG, PUBLIC_KEY_LEN, SIG_LEN, 3 | }; 4 | use crate::errors::{Error, FormatError}; 5 | use crate::{KeyNumber, PrivateKey, PublicKey, Signature}; 6 | use alloc::vec::Vec; 7 | use base64ct::Encoding; 8 | use zeroize::Zeroizing; 9 | 10 | /// A structure that can be converted to and from bytes and the `signify` file format. 11 | pub trait Codeable: Sized + Sealed { 12 | /// Parses a blob of serialized bytes into a structure. 13 | /// 14 | /// When working with signature files, [`from_base64`](Self::from_base64) should be 15 | /// prefered for compatibility with other implementations. 16 | fn from_bytes(bytes: &[u8]) -> Result; 17 | 18 | /// Parses a base64 encoded string into a structure. 19 | /// 20 | /// Returns the structure and number of bytes read. 21 | /// This can be helpful when dealing with embedded signatures. 22 | /// 23 | /// The parsing enforces that the `signify` file format is adhered to. 24 | /// 25 | /// Said format is roughly defined as such: 26 | /// ```text 27 | /// untrusted comment: <\n> 28 | /// <\n> 29 | /// ``` 30 | fn from_base64(encoded: &str) -> Result<(Self, u64), Error> { 31 | read_base64_contents(encoded).and_then(|(bytes, bytes_read)| { 32 | let bytes = Self::from_bytes(&bytes)?; 33 | Ok((bytes, bytes_read)) 34 | }) 35 | } 36 | 37 | /// Converts the structure into a blob of bytes and returns them. 38 | /// 39 | /// When working with signature files, [`to_file_encoding`](Self::to_file_encoding) 40 | /// should be prefered for compatibility with other implementations. 41 | fn as_bytes(&self) -> Vec; 42 | 43 | /// Converts the structure into a base64 encoded container and returned the raw bytes. 44 | /// 45 | /// The provided comment is added as the untrusted comment in the container. 46 | /// 47 | /// The container format can be seen in the [decoder's documentation]. 48 | /// 49 | /// [decoder's documentation]: Self::from_base64 50 | fn to_file_encoding(&self, comment: &str) -> Vec { 51 | let bytes = self.as_bytes(); 52 | 53 | let mut file_bytes = Vec::new(); 54 | 55 | file_bytes.extend_from_slice(COMMENT_HEADER.as_bytes()); 56 | file_bytes.extend_from_slice(comment.as_bytes()); 57 | file_bytes.push(b'\n'); 58 | 59 | let out = base64ct::Base64::encode_string(&bytes); 60 | file_bytes.extend_from_slice(out.as_bytes()); 61 | file_bytes.push(b'\n'); 62 | 63 | file_bytes 64 | } 65 | } 66 | 67 | use sealed::Sealed; 68 | mod sealed { 69 | pub trait Sealed {} 70 | } 71 | 72 | impl Sealed for PublicKey {} 73 | impl Sealed for PrivateKey {} 74 | impl Sealed for Signature {} 75 | 76 | struct SliceReader<'a>(&'a [u8]); 77 | 78 | impl SliceReader<'_> { 79 | fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Error> { 80 | if buf.len() > self.0.len() { 81 | return Err(Error::InsufficentData); 82 | } 83 | let (a, b) = self.0.split_at(buf.len()); 84 | 85 | buf.copy_from_slice(a); 86 | 87 | self.0 = b; 88 | Ok(()) 89 | } 90 | } 91 | 92 | impl Codeable for PublicKey { 93 | fn from_bytes(bytes: &[u8]) -> Result { 94 | let mut buf = SliceReader(bytes); 95 | 96 | let mut _pkgalg = [0u8; 2]; 97 | let mut keynum = [0; KeyNumber::LEN]; 98 | let mut public_key = [0; PUBLIC_KEY_LEN]; 99 | 100 | buf.read_exact(&mut _pkgalg)?; 101 | buf.read_exact(&mut keynum)?; 102 | buf.read_exact(&mut public_key)?; 103 | 104 | Ok(Self { 105 | keynum: KeyNumber::new(keynum), 106 | key: public_key, 107 | }) 108 | } 109 | 110 | fn as_bytes(&self) -> Vec { 111 | let mut bytes = Vec::new(); 112 | 113 | bytes.extend_from_slice(&PKGALG); 114 | bytes.extend_from_slice(self.keynum.as_ref()); 115 | bytes.extend_from_slice(&self.key); 116 | 117 | bytes 118 | } 119 | } 120 | 121 | impl Codeable for PrivateKey { 122 | fn from_bytes(bytes: &[u8]) -> Result { 123 | let mut buf = SliceReader(bytes); 124 | 125 | let mut public_key_alg = [0; 2]; 126 | let mut kdf_alg = [0; 2]; 127 | let mut salt = [0; 16]; 128 | let mut checksum = [0; 8]; 129 | let mut keynum = [0; KeyNumber::LEN]; 130 | let mut complete_key = Zeroizing::new([0; FULL_KEY_LEN]); 131 | 132 | buf.read_exact(&mut public_key_alg)?; 133 | buf.read_exact(&mut kdf_alg)?; 134 | let kdf_rounds = { 135 | let mut bytes = [0u8; core::mem::size_of::()]; 136 | buf.read_exact(&mut bytes)?; 137 | u32::from_be_bytes(bytes) 138 | }; 139 | buf.read_exact(&mut salt)?; 140 | buf.read_exact(&mut checksum)?; 141 | buf.read_exact(&mut keynum)?; 142 | buf.read_exact(&mut complete_key[..])?; 143 | 144 | // Sanity check the keypair wasn't corrupted, only if it was unencrypted. 145 | if kdf_rounds == 0 { 146 | PrivateKey::from_key_bytes(&complete_key).map(drop)?; 147 | } 148 | 149 | Ok(Self { 150 | public_key_alg, 151 | kdf_alg, 152 | kdf_rounds, 153 | salt, 154 | checksum, 155 | keynum: KeyNumber::new(keynum), 156 | complete_key, 157 | }) 158 | } 159 | 160 | fn as_bytes(&self) -> Vec { 161 | let mut bytes = Vec::new(); 162 | 163 | bytes.extend_from_slice(&self.public_key_alg); 164 | bytes.extend_from_slice(&self.kdf_alg); 165 | bytes.extend_from_slice(&self.kdf_rounds.to_be_bytes()); 166 | bytes.extend_from_slice(&self.salt); 167 | bytes.extend_from_slice(&self.checksum); 168 | bytes.extend_from_slice(self.keynum.as_ref()); 169 | bytes.extend_from_slice(self.complete_key.as_ref()); 170 | 171 | bytes 172 | } 173 | } 174 | 175 | impl Codeable for Signature { 176 | fn from_bytes(bytes: &[u8]) -> Result { 177 | let mut buf = SliceReader(bytes); 178 | 179 | let mut _pkgalg = [0u8; 2]; 180 | let mut keynum = [0; KeyNumber::LEN]; 181 | let mut sig = [0; SIG_LEN]; 182 | 183 | buf.read_exact(&mut _pkgalg)?; 184 | buf.read_exact(&mut keynum)?; 185 | buf.read_exact(&mut sig)?; 186 | 187 | Ok(Self { 188 | keynum: KeyNumber::new(keynum), 189 | sig, 190 | }) 191 | } 192 | 193 | fn as_bytes(&self) -> Vec { 194 | let mut bytes = Vec::new(); 195 | 196 | bytes.extend_from_slice(&PKGALG); 197 | bytes.extend_from_slice(self.keynum.as_ref()); 198 | bytes.extend_from_slice(&self.sig); 199 | 200 | bytes 201 | } 202 | } 203 | 204 | fn read_base64_contents(encoded: &str) -> Result<(Vec, u64), Error> { 205 | let mut lines = encoded.split_terminator('\n'); 206 | 207 | // Newline ending is implicitly checked by `split`. 208 | let comment_line = lines.next().ok_or(FormatError::MissingNewline)?; 209 | 210 | if !comment_line.starts_with(COMMENT_HEADER) { 211 | return Err(FormatError::Comment { 212 | expected: COMMENT_HEADER, 213 | } 214 | .into()); 215 | } 216 | 217 | if comment_line.len() > COMMENT_HEADER.len() + COMMENT_MAX_LEN { 218 | return Err(FormatError::LineLength.into()); 219 | } 220 | 221 | let base64_line = lines.next().ok_or(FormatError::MissingNewline)?; 222 | 223 | let data = 224 | base64ct::Base64::decode_vec(base64_line.trim_end()).map_err(|_| FormatError::Base64)?; 225 | 226 | match data.get(0..2) { 227 | // Make sure the specified algorithm matches what we support 228 | Some(alg) if alg == PKGALG => { 229 | // Return the number of bytes this function ended up parsing, including the two newlines parsed. 230 | let bytes_read = comment_line.len() + base64_line.len() + 2; 231 | Ok((data, bytes_read as u64)) 232 | } 233 | Some(_) | None => Err(Error::UnsupportedAlgorithm), 234 | } 235 | } 236 | 237 | #[cfg(test)] 238 | mod tests { 239 | use super::*; 240 | use crate::NewKeyOpts; 241 | use alloc::string::String; 242 | 243 | #[test] 244 | fn public_key_codeable() { 245 | let key = PublicKey { 246 | keynum: KeyNumber::new([3u8; KeyNumber::LEN]), 247 | key: [4u8; PUBLIC_KEY_LEN], 248 | }; 249 | 250 | let bytes = key.as_bytes(); 251 | let deserialized = PublicKey::from_bytes(&bytes).unwrap(); 252 | assert_eq!(key, deserialized); 253 | 254 | let file_encoding = String::from_utf8(key.to_file_encoding("my comment")).unwrap(); 255 | let (deserialized, bytes_read) = PublicKey::from_base64(&file_encoding).unwrap(); 256 | assert_eq!(key, deserialized); 257 | assert!(bytes_read > 0); 258 | } 259 | 260 | #[test] 261 | fn private_key_codeable() { 262 | let mut rng = rand_core::OsRng; 263 | 264 | { 265 | let key = PrivateKey::generate( 266 | &mut rng, 267 | NewKeyOpts::Encrypted { 268 | passphrase: String::from("supersecure"), 269 | kdf_rounds: 16, 270 | }, 271 | ) 272 | .unwrap(); 273 | 274 | let bytes = key.as_bytes(); 275 | let deserialized = PrivateKey::from_bytes(&bytes).unwrap(); 276 | assert_eq!(deserialized.complete_key, key.complete_key); 277 | } 278 | 279 | let key = PrivateKey::generate(&mut rng, NewKeyOpts::NoEncryption).unwrap(); 280 | 281 | let bytes = key.as_bytes(); 282 | let deserialized = PrivateKey::from_bytes(&bytes).unwrap(); 283 | assert_eq!(key, deserialized); 284 | 285 | let file_encoding = String::from_utf8(key.to_file_encoding("my comment")).unwrap(); 286 | let (deserialized, bytes_read) = PrivateKey::from_base64(&file_encoding).unwrap(); 287 | assert_eq!(key, deserialized); 288 | assert!(bytes_read > 0); 289 | } 290 | 291 | #[test] 292 | fn signature_codeable() { 293 | let sig = Signature { 294 | keynum: KeyNumber::new([3u8; KeyNumber::LEN]), 295 | sig: [9u8; SIG_LEN], 296 | }; 297 | 298 | let bytes = sig.as_bytes(); 299 | let deserialized = Signature::from_bytes(&bytes).unwrap(); 300 | assert_eq!(sig, deserialized); 301 | 302 | let file_encoding = String::from_utf8(sig.to_file_encoding("my comment")).unwrap(); 303 | let (deserialized, bytes_read) = Signature::from_base64(&file_encoding).unwrap(); 304 | assert_eq!(sig, deserialized); 305 | assert!(bytes_read > 0); 306 | } 307 | 308 | const BASE64_CASES: &[(&str, Result<(), Error>)] = &[ 309 | ( 310 | "untrusted comment: my comment\nRWQgaW4gV29uZGVybGFuZA==\n", 311 | Ok(()), 312 | ), 313 | ( 314 | "nottherightheader: aaaaaaa", 315 | Err(Error::InvalidFormat(FormatError::Comment { 316 | expected: COMMENT_HEADER, 317 | })), 318 | ), 319 | ( 320 | "untrusted comment: makethisverylong", 321 | Err(Error::InvalidFormat(FormatError::LineLength)), 322 | ), 323 | ( 324 | "untrusted comment: bbbbbb\rbbbbbbbb", 325 | Err(Error::InvalidFormat(FormatError::MissingNewline)), 326 | ), 327 | ( 328 | "untrusted comment: cccccc\n", 329 | Err(Error::InvalidFormat(FormatError::MissingNewline)), 330 | ), 331 | ( 332 | "untrusted comment: dddddd\nRWQgaW4gV2ZGVybGFuZA==", 333 | Err(Error::InvalidFormat(FormatError::Base64)), 334 | ), 335 | ( 336 | "untrusted comment: eeeeee\nUGF0IGluIFdvbmRlcmxhbmQ=", 337 | Err(Error::UnsupportedAlgorithm), 338 | ), 339 | ]; 340 | 341 | #[test] 342 | fn base64_reading() { 343 | for (encoded, expected) in BASE64_CASES { 344 | let encoded = encoded.replace("makethisverylong", &"a".repeat(1025)); 345 | let result = read_base64_contents(&encoded).map(|_| ()); 346 | 347 | assert_eq!(result, *expected, "{} produced the wrong result", encoded); 348 | } 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /libsignify/src/errors.rs: -------------------------------------------------------------------------------- 1 | use crate::consts::KeyNumber; 2 | use core::fmt::{self, Display}; 3 | 4 | #[cfg(feature = "std")] 5 | extern crate std; 6 | 7 | /// The error type which is returned when some `signify` operation fails. 8 | #[derive(Debug, PartialEq, Eq)] 9 | pub enum Error { 10 | /// Not enough data was found to parse a structure. 11 | InsufficentData, 12 | /// Parsing a structure's data yielded an error. 13 | InvalidFormat(FormatError), 14 | /// The key algorithm used was unknown and unsupported. 15 | UnsupportedAlgorithm, 16 | /// Attempted to verify a signature with the wrong public key. 17 | MismatchedKey { 18 | /// ID of the key which created the signature. 19 | expected: KeyNumber, 20 | /// ID of the key that tried to verify the signature, but was wrong. 21 | found: KeyNumber, 22 | }, 23 | /// The wrong public key was associated with the full keypair. 24 | WrongKey, 25 | /// The signature didn't match the expected result. 26 | /// 27 | /// This could be the result of data corruption or malicious tampering. 28 | /// 29 | /// The contents of the message should not be trusted if this is encountered. 30 | BadSignature, 31 | /// Provided password was empty or couldn't decrypt a private key. 32 | BadPassword, 33 | } 34 | 35 | impl Display for Error { 36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 37 | match self { 38 | Error::InsufficentData => f.write_str("insufficent data while parsing structure"), 39 | Error::InvalidFormat(e) => Display::fmt(e, f), 40 | Error::UnsupportedAlgorithm => f.write_str("encountered unsupported key algorithm"), 41 | Error::MismatchedKey { expected, found } => { 42 | write!(f, 43 | "failed to verify signature: the wrong key was used. Expected {:?}, but found {:?}", 44 | expected, 45 | found, 46 | ) 47 | } 48 | Error::WrongKey => f.write_str("public key does not belong to the private key"), 49 | Error::BadSignature => f.write_str("signature verification failed"), 50 | Error::BadPassword => f.write_str("password was empty or incorrect for key"), 51 | } 52 | } 53 | } 54 | 55 | #[cfg(feature = "std")] 56 | impl std::error::Error for Error {} 57 | 58 | /// The error that is returned when a file's contents didn't adhere 59 | /// to the `signify` file container format. 60 | #[derive(Debug, PartialEq, Eq)] 61 | pub enum FormatError { 62 | /// A comment line exceeded the maximum length or a data line was empty. 63 | LineLength, 64 | /// File was missing the required `untrusted comment: ` preamble. 65 | Comment { 66 | /// The expected comment header. 67 | expected: &'static str, 68 | }, 69 | /// File was missing a required line or wasn't correctly newline terminated. 70 | MissingNewline, 71 | /// Provided data wasn't valid base64. 72 | Base64, 73 | } 74 | 75 | impl Display for FormatError { 76 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 77 | match self { 78 | FormatError::LineLength => { 79 | f.write_str("encountered an invalidly formatted line of data") 80 | } 81 | FormatError::Comment { expected } => { 82 | write!(f, "line missing comment; expected {}", expected) 83 | } 84 | FormatError::MissingNewline => f.write_str("expected newline was not found"), 85 | FormatError::Base64 => f.write_str("encountered invalid base64 data"), 86 | } 87 | } 88 | } 89 | 90 | #[cfg(feature = "std")] 91 | impl std::error::Error for FormatError {} 92 | 93 | impl From for Error { 94 | fn from(e: FormatError) -> Self { 95 | Self::InvalidFormat(e) 96 | } 97 | } 98 | 99 | #[cfg(test)] 100 | mod tests { 101 | use super::*; 102 | use core::fmt::{Debug, Display}; 103 | use static_assertions::assert_impl_all; 104 | 105 | #[cfg(feature = "std")] 106 | assert_impl_all!(Error: std::error::Error); 107 | #[cfg(feature = "std")] 108 | assert_impl_all!(FormatError: std::error::Error); 109 | 110 | assert_impl_all!(Error: Debug, Display, PartialEq, Send, Sync); 111 | assert_impl_all!(FormatError: Debug, Display, PartialEq, Send, Sync); 112 | } 113 | -------------------------------------------------------------------------------- /libsignify/src/key.rs: -------------------------------------------------------------------------------- 1 | use crate::consts::{KeyNumber, FULL_KEY_LEN, KDFALG, PKGALG, PUBLIC_KEY_LEN, SIG_LEN}; 2 | use crate::errors::Error; 3 | 4 | use alloc::string::String; 5 | use core::convert::TryInto; 6 | use rand_core::{CryptoRng, RngCore}; 7 | use sha2::{Digest, Sha512}; 8 | use zeroize::{Zeroize, Zeroizing}; 9 | 10 | /// The public half of a keypair. 11 | /// 12 | /// You will need this if you are trying to verify a signature. 13 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] 14 | pub struct PublicKey { 15 | pub(crate) keynum: KeyNumber, 16 | pub(crate) key: [u8; PUBLIC_KEY_LEN], 17 | } 18 | 19 | impl PublicKey { 20 | /// The public key's bytes. 21 | pub fn key(&self) -> [u8; PUBLIC_KEY_LEN] { 22 | self.key 23 | } 24 | 25 | /// The public key's identifying number. 26 | pub fn keynum(&self) -> KeyNumber { 27 | self.keynum 28 | } 29 | } 30 | 31 | /// Key derivation options available when creating a new key. 32 | #[derive(Clone)] 33 | pub enum NewKeyOpts { 34 | /// Don't encrypt the secret key. 35 | NoEncryption, 36 | /// Encrypt the secret key with a passphrase. 37 | Encrypted { 38 | /// Passphrase to encrypt the key with. 39 | passphrase: String, 40 | /// The number of key derivation rounds to apply to the password. 41 | /// 42 | /// If you're unsure of what this should be set to, use [the default]. 43 | /// 44 | /// [the default]: crate::consts::DEFAULT_KDF_ROUNDS 45 | kdf_rounds: u32, 46 | }, 47 | } 48 | 49 | impl Drop for NewKeyOpts { 50 | fn drop(&mut self) { 51 | if let NewKeyOpts::Encrypted { passphrase, .. } = self { 52 | passphrase.zeroize(); 53 | } 54 | } 55 | } 56 | 57 | impl core::fmt::Debug for NewKeyOpts { 58 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 59 | match self { 60 | Self::NoEncryption => f.write_str("NoEncryption"), 61 | Self::Encrypted { kdf_rounds, .. } => f 62 | .debug_struct("Encrypted") 63 | .field("passphrase", &"") 64 | .field("kdf_rounds", kdf_rounds) 65 | .finish(), 66 | } 67 | } 68 | } 69 | 70 | struct UnencryptedKey(Zeroizing<[u8; FULL_KEY_LEN]>); 71 | 72 | /// The full secret keypair. 73 | /// 74 | /// You will need this if you want to create signatures. 75 | #[derive(Clone)] 76 | #[cfg_attr(test, derive(Debug, PartialEq, Eq))] // Makes the encoding tests nicer. 77 | pub struct PrivateKey { 78 | pub(crate) public_key_alg: [u8; 2], 79 | pub(crate) kdf_alg: [u8; 2], 80 | pub(crate) kdf_rounds: u32, 81 | pub(crate) salt: [u8; 16], 82 | pub(crate) checksum: [u8; 8], 83 | pub(super) keynum: KeyNumber, 84 | pub(super) complete_key: Zeroizing<[u8; FULL_KEY_LEN]>, 85 | } 86 | 87 | impl PrivateKey { 88 | /// Generates a new random secret (private) key with the provided options. 89 | /// 90 | /// # Errors 91 | /// 92 | /// This only returns an error if the provided password was empty. 93 | pub fn generate( 94 | rng: &mut R, 95 | derivation_info: NewKeyOpts, 96 | ) -> Result { 97 | let keynum = KeyNumber::generate(rng); 98 | 99 | let key_pair = ed25519_dalek::SigningKey::generate(rng); 100 | 101 | let mut complete_key = UnencryptedKey(Zeroizing::new(key_pair.to_keypair_bytes())); 102 | 103 | let checksum = Self::calculate_checksum(&complete_key); 104 | 105 | let mut salt = [0; 16]; 106 | rng.fill_bytes(&mut salt); 107 | 108 | let kdf_rounds = if let NewKeyOpts::Encrypted { 109 | passphrase, 110 | kdf_rounds, 111 | } = &derivation_info 112 | { 113 | let kdf_rounds = *kdf_rounds; 114 | Self::inner_kdf_mix(&mut complete_key.0[..32], kdf_rounds, &salt, passphrase)?; 115 | kdf_rounds 116 | } else { 117 | 0 118 | }; 119 | 120 | Ok(Self { 121 | public_key_alg: PKGALG, 122 | kdf_alg: KDFALG, 123 | kdf_rounds, 124 | salt, 125 | checksum, 126 | keynum, 127 | // The state of the key doesn't matter at this stage. 128 | complete_key: complete_key.0, 129 | }) 130 | } 131 | 132 | /// Decrypts a secret key that was stored in encrypted form with the passphrase. 133 | /// 134 | /// # Errors 135 | /// 136 | /// This returns an error if the provided password was empty or if it failed to decrypt the key. 137 | pub fn decrypt_with_password(&mut self, passphrase: &str) -> Result<(), Error> { 138 | let mut encrypted_key = self.complete_key.clone(); // Cheap :) 139 | 140 | match Self::inner_kdf_mix( 141 | &mut encrypted_key[..32], 142 | self.kdf_rounds, 143 | &self.salt, 144 | passphrase, 145 | ) { 146 | Ok(_) => { 147 | // Since the decryption worked, its now "unencrypted", even if the passphrase was wrong 148 | // and the value is garbage. 149 | let decrypted_key = UnencryptedKey(encrypted_key); 150 | let current_checksum = Self::calculate_checksum(&decrypted_key); 151 | 152 | // Non-constant time is fine since checksum is public. 153 | if current_checksum != self.checksum { 154 | return Err(Error::BadPassword); 155 | } 156 | 157 | // Confirmed the decryption worked and the keys are matching. 158 | PrivateKey::from_key_bytes(&decrypted_key.0).map(drop)?; 159 | 160 | self.complete_key = decrypted_key.0; 161 | 162 | Ok(()) 163 | } 164 | Err(e) => Err(e), 165 | } 166 | } 167 | 168 | /// Creates a well-typed signing keypair from raw bytes. 169 | /// 170 | /// This also validates the provided public/verifying key matches the 171 | /// private/signing key. 172 | pub(crate) fn from_key_bytes( 173 | complete_key: &[u8; FULL_KEY_LEN], 174 | ) -> Result { 175 | ed25519_dalek::SigningKey::from_keypair_bytes(complete_key).map_err(|_| Error::WrongKey) 176 | } 177 | 178 | fn calculate_checksum(complete_key: &UnencryptedKey) -> [u8; 8] { 179 | let digest = Sha512::digest(complete_key.0.as_ref()); 180 | let mut checksum = [0; 8]; 181 | checksum.copy_from_slice(&digest.as_slice()[0..8]); 182 | checksum 183 | } 184 | 185 | fn inner_kdf_mix( 186 | secret_key: &mut [u8], 187 | rounds: u32, 188 | salt: &[u8], 189 | passphrase: &str, 190 | ) -> Result<(), Error> { 191 | if rounds == 0 { 192 | return Ok(()); 193 | } 194 | 195 | let mut xorkey = Zeroizing::new([0; FULL_KEY_LEN]); 196 | let mut workspace = Zeroizing::new([0; FULL_KEY_LEN]); 197 | 198 | bcrypt_pbkdf::bcrypt_pbkdf_with_memory( 199 | passphrase, 200 | salt, 201 | rounds, 202 | xorkey.as_mut_slice(), 203 | workspace.as_mut_slice(), 204 | ) 205 | .map_err(|_| Error::BadPassword)?; 206 | 207 | for (prv, xor) in secret_key.iter_mut().zip(xorkey.iter()) { 208 | *prv ^= xor; 209 | } 210 | 211 | Ok(()) 212 | } 213 | 214 | /// Returns the public half of this secret keypair. 215 | pub fn public(&self) -> PublicKey { 216 | // This `unwrap()` gets erased in release mode. 217 | PublicKey { 218 | key: self.complete_key[32..].try_into().unwrap(), 219 | keynum: self.keynum, 220 | } 221 | } 222 | 223 | /// Returns if this key was stored encrypted. 224 | pub fn is_encrypted(&self) -> bool { 225 | self.kdf_rounds != 0 226 | } 227 | } 228 | 229 | /// A signature 230 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] 231 | pub struct Signature { 232 | pub(crate) keynum: KeyNumber, 233 | pub(crate) sig: [u8; SIG_LEN], 234 | } 235 | 236 | impl Signature { 237 | /// The ID of the keypair which created this signature. 238 | /// 239 | /// This is useful to determine if you have the right key to verify a signature. 240 | pub fn signer_keynum(&self) -> KeyNumber { 241 | self.keynum 242 | } 243 | 244 | /// Returns the signature's raw bytes. 245 | pub fn signature(&self) -> [u8; SIG_LEN] { 246 | self.sig 247 | } 248 | 249 | pub(super) fn new(keynum: KeyNumber, sig: [u8; SIG_LEN]) -> Self { 250 | Self { keynum, sig } 251 | } 252 | } 253 | 254 | #[cfg(test)] 255 | mod tests { 256 | use crate::consts::DEFAULT_KDF_ROUNDS; 257 | use crate::test_utils::StepperRng; 258 | 259 | use super::*; 260 | use alloc::string::ToString; 261 | use core::fmt::Debug; 262 | use core::hash::Hash; 263 | use static_assertions::assert_impl_all; 264 | 265 | assert_impl_all!( 266 | PublicKey: Clone, 267 | Copy, 268 | Debug, 269 | Eq, 270 | PartialEq, 271 | Hash, 272 | Send, 273 | Sync 274 | ); 275 | 276 | assert_impl_all!(PrivateKey: Clone, Send, Sync); 277 | 278 | assert_impl_all!(NewKeyOpts: Clone, Debug, Send, Sync); 279 | 280 | assert_impl_all!( 281 | Signature: Clone, 282 | Copy, 283 | Debug, 284 | Eq, 285 | PartialEq, 286 | Hash, 287 | Send, 288 | Sync 289 | ); 290 | 291 | const PASSPHRASE: &str = "muchsecret"; 292 | 293 | #[test] 294 | fn check_key_generation_passphrase_concealment() { 295 | let new_opts = NewKeyOpts::Encrypted { 296 | passphrase: PASSPHRASE.to_string(), 297 | kdf_rounds: DEFAULT_KDF_ROUNDS, 298 | }; 299 | let debug_output = alloc::format!("{:?}", new_opts); 300 | assert!(!debug_output.contains(PASSPHRASE)); 301 | } 302 | 303 | #[test] 304 | fn check_simple_private_key_getters() { 305 | let mut rng = StepperRng::default(); 306 | let unencrypted_key = PrivateKey::generate(&mut rng, NewKeyOpts::NoEncryption).unwrap(); 307 | 308 | assert_eq!( 309 | unencrypted_key.public().key(), 310 | unencrypted_key.complete_key[32..] 311 | ); // Ed25519 keys are private || public. 312 | 313 | assert!(!unencrypted_key.is_encrypted()); 314 | 315 | let encrypted_key = PrivateKey::generate( 316 | &mut rng, 317 | NewKeyOpts::Encrypted { 318 | passphrase: PASSPHRASE.to_string(), 319 | kdf_rounds: DEFAULT_KDF_ROUNDS, 320 | }, 321 | ) 322 | .unwrap(); 323 | assert!(encrypted_key.is_encrypted()); 324 | } 325 | 326 | #[test] 327 | fn check_key_generation_opts() { 328 | let mut rng = StepperRng::default(); 329 | let unencrypted_key = PrivateKey::generate(&mut rng, NewKeyOpts::NoEncryption).unwrap(); 330 | assert_eq!(unencrypted_key.kdf_rounds, 0); // `0` represents not encrypted. 331 | assert_eq!(unencrypted_key.kdf_alg, KDFALG); 332 | assert_eq!(unencrypted_key.public_key_alg, PKGALG); 333 | 334 | let encrypted_key_1 = PrivateKey::generate( 335 | &mut rng, 336 | NewKeyOpts::Encrypted { 337 | passphrase: PASSPHRASE.to_string(), 338 | kdf_rounds: DEFAULT_KDF_ROUNDS, 339 | }, 340 | ) 341 | .unwrap(); 342 | assert_eq!(encrypted_key_1.kdf_rounds, DEFAULT_KDF_ROUNDS); 343 | assert_eq!(encrypted_key_1.kdf_alg, KDFALG); 344 | assert_eq!(encrypted_key_1.public_key_alg, PKGALG); 345 | 346 | // Check non-standard KDF rounds are respected. 347 | let encrypted_key_2 = PrivateKey::generate( 348 | &mut rng, 349 | NewKeyOpts::Encrypted { 350 | passphrase: PASSPHRASE.to_string(), 351 | kdf_rounds: 7, 352 | }, 353 | ) 354 | .unwrap(); 355 | assert_eq!(encrypted_key_2.kdf_rounds, 7); 356 | assert_eq!(encrypted_key_1.kdf_alg, KDFALG); 357 | assert_eq!(encrypted_key_2.public_key_alg, PKGALG); 358 | 359 | // Salts should be random. 360 | assert_ne!(encrypted_key_1.salt, encrypted_key_2.salt); 361 | // Key numbers should be unique. 362 | assert_ne!(encrypted_key_1.keynum, encrypted_key_2.keynum); 363 | // The keys themselves should be random and unique. 364 | assert_ne!(encrypted_key_1.complete_key, encrypted_key_2.complete_key); 365 | assert_ne!(encrypted_key_1.checksum, encrypted_key_2.checksum); 366 | } 367 | 368 | struct ConstantRng; 369 | 370 | impl ConstantRng { 371 | const VALUE: u8 = 3; 372 | } 373 | 374 | impl rand_core::RngCore for ConstantRng { 375 | fn next_u32(&mut self) -> u32 { 376 | Self::VALUE.into() 377 | } 378 | 379 | fn next_u64(&mut self) -> u64 { 380 | Self::VALUE.into() 381 | } 382 | 383 | fn fill_bytes(&mut self, dest: &mut [u8]) { 384 | for b in dest { 385 | *b = Self::VALUE; 386 | } 387 | } 388 | 389 | fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { 390 | for b in dest { 391 | *b = Self::VALUE; 392 | } 393 | 394 | Ok(()) 395 | } 396 | } 397 | 398 | impl rand_core::CryptoRng for ConstantRng {} 399 | 400 | #[test] 401 | fn check_key_encryption_roundtrip() { 402 | const ACTUAL_KEY: [u8; 32] = [ConstantRng::VALUE; 32]; 403 | let mut rng = ConstantRng; 404 | 405 | let mut encrypted_key = PrivateKey::generate( 406 | &mut rng, 407 | NewKeyOpts::Encrypted { 408 | passphrase: PASSPHRASE.to_string(), 409 | kdf_rounds: DEFAULT_KDF_ROUNDS, 410 | }, 411 | ) 412 | .unwrap(); 413 | 414 | // Easy check that its actually being encrypted when requested. 415 | assert_ne!(encrypted_key.complete_key.as_ref()[..32], ACTUAL_KEY); 416 | 417 | // ... and then make sure it properly decrypts. 418 | encrypted_key.decrypt_with_password(PASSPHRASE).unwrap(); 419 | assert_eq!(encrypted_key.complete_key.as_ref()[..32], ACTUAL_KEY); 420 | } 421 | 422 | #[test] 423 | fn check_wrong_passphrase_errors() { 424 | let mut rng = StepperRng::default(); 425 | let mut encrypted_key = PrivateKey::generate( 426 | &mut rng, 427 | NewKeyOpts::Encrypted { 428 | passphrase: PASSPHRASE.to_string(), 429 | kdf_rounds: DEFAULT_KDF_ROUNDS, 430 | }, 431 | ) 432 | .unwrap(); 433 | 434 | assert!(matches!( 435 | encrypted_key.decrypt_with_password("wrong"), 436 | Err(Error::BadPassword) 437 | )); 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /libsignify/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Create cryptographic signatures for files and verify them. 2 | //! 3 | //! This is based on [signify], the OpenBSD tool to sign and verify signatures on files. 4 | //! It is based on the [Ed25519 public-key signature system][ed25519] by Bernstein et al. 5 | //! 6 | //! `libsignify` can verify and create signatures that are interoperable with BSD signify. 7 | //! You can read more about the ideas and concepts behind `signify` in [Securing OpenBSD From Us To You](https://www.openbsd.org/papers/bsdcan-signify.html). 8 | //! 9 | //! This crate is `#![no_std]` by default, but still relies on `liballoc` so your platform must 10 | //! provide an allocator to use `libsignify`. 11 | //! 12 | //! To enable support for `std::error::Error`, enable the `std` feature. 13 | //! 14 | //! ## Examples 15 | //! A simple CLI that verifies some example data: 16 | //! ```rust 17 | #![doc = include_str!("../examples/basic.rs")] 18 | //! ``` 19 | //! 20 | //! [signify]: https://github.com/aperezdc/signify 21 | //! [ed25519]: https://ed25519.cr.yp.to/ 22 | #![warn(missing_docs)] 23 | #![deny(rustdoc::broken_intra_doc_links)] 24 | #![no_std] 25 | 26 | extern crate alloc; 27 | 28 | pub mod consts; 29 | pub use consts::KeyNumber; 30 | 31 | mod encoding; 32 | pub use encoding::Codeable; 33 | 34 | mod errors; 35 | pub use errors::{Error, FormatError}; 36 | 37 | mod key; 38 | pub use key::{NewKeyOpts, PrivateKey, PublicKey, Signature}; 39 | 40 | #[cfg(test)] 41 | pub(crate) mod test_utils; 42 | 43 | use ed25519_dalek::{Signer as _, Verifier as _}; 44 | 45 | impl PrivateKey { 46 | /// Signs a message with this secret key and returns the signature. 47 | pub fn sign(&self, msg: &[u8]) -> Signature { 48 | // This panics because signing is otherwise infallible if the key is valid. 49 | // 50 | // All constructors of `PrivateKey` return a valid one, so this is better then forcing 51 | // a caller to handle an impossible error. 52 | let keypair = PrivateKey::from_key_bytes(&self.complete_key) 53 | .expect("invalid private keypair used for signing"); 54 | let sig = keypair.sign(msg).to_bytes(); 55 | Signature::new(self.keynum, sig) 56 | } 57 | } 58 | 59 | impl PublicKey { 60 | /// Use this key to verify that the provided signature for the given message 61 | /// is authentic. 62 | /// 63 | /// # Errors 64 | /// 65 | /// This method errors if this key's number didn't match the ID of the key 66 | /// which created the signature or if the signature couldn't be verified. 67 | pub fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), Error> { 68 | let current_keynum = self.keynum(); 69 | let expected_keynum = signature.keynum; 70 | 71 | if expected_keynum != current_keynum { 72 | return Err(Error::MismatchedKey { 73 | expected: expected_keynum, 74 | found: current_keynum, 75 | }); 76 | } 77 | 78 | // Both the key data and signature data are not verified yet, 79 | // so the ed25519 math can still go wrong. 80 | // In that case all we need to communicate is that it was a bad signature. 81 | 82 | let public_key = ed25519_dalek::VerifyingKey::from_bytes(&self.key()) 83 | .map_err(|_| Error::BadSignature)?; 84 | let signature = ed25519_dalek::Signature::from_bytes(&signature.signature()); 85 | public_key 86 | .verify(msg, &signature) 87 | .map_err(|_| Error::BadSignature) 88 | } 89 | } 90 | 91 | #[cfg(test)] 92 | mod tests { 93 | use super::*; 94 | use crate::test_utils::StepperRng; 95 | 96 | const MSG: &[u8] = b"signify!!!"; 97 | 98 | #[test] 99 | fn check_signature_roundtrip() { 100 | let mut rng = StepperRng::default(); 101 | 102 | let secret_key = PrivateKey::generate(&mut rng, NewKeyOpts::NoEncryption).unwrap(); 103 | let public_key = secret_key.public(); 104 | let signature = secret_key.sign(MSG); 105 | 106 | assert_eq!(signature.signer_keynum(), public_key.keynum()); 107 | 108 | assert!(public_key.verify(MSG, &signature).is_ok()); 109 | } 110 | 111 | #[test] 112 | fn check_signature_mismatched_keynum() { 113 | let mut rng = StepperRng::default(); 114 | 115 | let secret_key = PrivateKey::generate(&mut rng, NewKeyOpts::NoEncryption).unwrap(); 116 | let public_key = secret_key.public(); 117 | let mut signature = secret_key.sign(MSG); 118 | 119 | let wrong_keynum = KeyNumber::new([0u8; KeyNumber::LEN]); 120 | 121 | signature.keynum = wrong_keynum; 122 | 123 | assert_eq!( 124 | public_key.verify(MSG, &signature), 125 | Err(Error::MismatchedKey { 126 | expected: wrong_keynum, 127 | found: public_key.keynum() 128 | }) 129 | ) 130 | } 131 | 132 | #[test] 133 | fn check_malformed_publickey() { 134 | let mut rng = StepperRng::default(); 135 | 136 | let secret_key = PrivateKey::generate(&mut rng, NewKeyOpts::NoEncryption).unwrap(); 137 | let mut public_key = secret_key.public(); 138 | let signature = secret_key.sign(MSG); 139 | 140 | // Mess the public key up so its not a curve point anymore. 141 | public_key.key = [ 142 | 136, 95, 131, 189, 208, 168, 196, 163, 180, 145, 35, 42, 113, 108, 172, 178, 62, 108, 143 | 7, 205, 20, 215, 240, 50, 149, 237, 146, 32, 181, 180, 91, 255, 144 | ]; 145 | 146 | assert_eq!(public_key.verify(MSG, &signature), Err(Error::BadSignature)); 147 | } 148 | 149 | #[test] 150 | fn check_malformed_signature() { 151 | let mut rng = StepperRng::default(); 152 | 153 | let secret_key = PrivateKey::generate(&mut rng, NewKeyOpts::NoEncryption).unwrap(); 154 | let public_key = secret_key.public(); 155 | let mut signature = secret_key.sign(MSG); 156 | 157 | let real_sig = signature.sig; 158 | 159 | // Make the signature fail the basic validations. 160 | signature.sig = [255u8; consts::SIG_LEN]; 161 | 162 | assert_eq!(public_key.verify(MSG, &signature), Err(Error::BadSignature)); 163 | 164 | signature.sig = real_sig; 165 | // Slightly modify the signature so that full verification fails. 166 | signature.sig[20] = 3; 167 | 168 | assert_eq!(public_key.verify(MSG, &signature), Err(Error::BadSignature)); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /libsignify/src/test_utils.rs: -------------------------------------------------------------------------------- 1 | /// A RNG that produces numbers by incrementing a value each time. 2 | /// 3 | /// Used as a fill-in for `OsRng` since it doesn't work well for the no_std tests :( 4 | pub struct StepperRng { 5 | current: u64, 6 | } 7 | 8 | impl Default for StepperRng { 9 | fn default() -> Self { 10 | Self { current: 5 } 11 | } 12 | } 13 | 14 | // :) 15 | impl rand_core::CryptoRng for StepperRng {} 16 | 17 | impl rand_core::RngCore for StepperRng { 18 | fn next_u32(&mut self) -> u32 { 19 | self.next_u64() as u32 20 | } 21 | 22 | fn next_u64(&mut self) -> u64 { 23 | let out = self.current; 24 | self.current = self.current.wrapping_add(1); 25 | out 26 | } 27 | 28 | fn fill_bytes(&mut self, dest: &mut [u8]) { 29 | rand_core::impls::fill_bytes_via_next(self, dest) 30 | } 31 | 32 | fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { 33 | self.fill_bytes(dest); 34 | Ok(()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | shared-version = true 2 | tag-name = "v{{version}}" 3 | pre-release-replacements = [ 4 | {file="../CHANGELOG.md", search="# Unreleased", replace="# {{version}} ({{date}})", exactly=1}, 5 | {file="../CHANGELOG.md", search="\\.\\.\\.main", replace="...v{{version}}", exactly=1}, 6 | {file="../CHANGELOG.md", search="\\A", replace="# Unreleased\n\n[Full changelog](https://github.com/badboy/signify-rs/compare/v{{version}}...main)\n\n", exactly=1} 7 | ] 8 | -------------------------------------------------------------------------------- /signify/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "signify" 3 | version = "0.6.0" 4 | authors = ["Jan-Erik Rediger ", "BlackHoleFox "] 5 | edition = "2021" 6 | 7 | keywords = ["cryptography", "security"] 8 | description = "Command-line implementation of the signify signature scheme" 9 | 10 | readme = "../README.md" 11 | license = "MIT" 12 | 13 | homepage = "https://github.com/badboy/signify-rs" 14 | repository = "https://github.com/badboy/signify-rs" 15 | 16 | [[bin]] 17 | name = "signify" 18 | doc = false 19 | 20 | [dependencies] 21 | clap = { version = "4.4.0", default-features = false, features = ["cargo", "derive", "std"] } 22 | rand_core = { version = "0.6", features = ["getrandom"] } 23 | rpassword = { version = "7", default-features = false } 24 | libsignify = { path = "../libsignify", version = "0.6.0", features = ["std"] } 25 | -------------------------------------------------------------------------------- /signify/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fs::{File, OpenOptions}; 3 | use std::io::BufReader; 4 | use std::io::{prelude::*, SeekFrom}; 5 | use std::path::{Path, PathBuf}; 6 | use std::process; 7 | 8 | use libsignify::{ 9 | consts::DEFAULT_KDF_ROUNDS, Codeable, NewKeyOpts, PrivateKey, PublicKey, Signature, 10 | }; 11 | 12 | use clap::{CommandFactory, Parser}; 13 | 14 | #[derive(Parser)] 15 | #[clap( 16 | name = "signify", 17 | override_usage = r#"signify -h 18 | signify -G [-n] [-c ] -p -s 19 | signify -S [-e] [-x ] -s -m 20 | signify -V [-e] [-x ] -p -m "# 21 | )] 22 | struct Args { 23 | /// Generate a new keypair 24 | #[clap(short = 'G')] 25 | generate: bool, 26 | /// Sign the specified message file 27 | #[clap(short = 'S')] 28 | sign: bool, 29 | /// Verify a message 30 | #[clap(short = 'V')] 31 | verify: bool, 32 | /// Public key produced by -G, and used by -V to check a signature 33 | #[clap(short, value_parser)] 34 | pubkey: Option, 35 | /// Secret (private) key produced by -G, and used by -S to sign a message 36 | #[clap(short, value_parser)] 37 | seckey: Option, 38 | /// Do not ask for a passphrase during key generation. Otherwise, signify 39 | /// will prompt the user for a passphrase to protect the secret key 40 | #[clap(short = 'n')] 41 | skip_key_encryption: bool, 42 | /// The file containing the message to create a signature over or to the one to verify with an existing signature 43 | #[clap(short, value_parser)] 44 | message_path: Option, 45 | /// When signing, embed the message after the signature. When verifying, 46 | /// extract the message from the signature 47 | #[clap(short)] 48 | embed_message: bool, 49 | /// The signature file to create or verify. The default is .sig 50 | #[clap(short = 'x', value_parser)] 51 | signature_path: Option, 52 | /// Specify the comment to be added during key generation 53 | #[clap(short)] 54 | comment: Option, 55 | } 56 | 57 | fn write_base64_file( 58 | file: &mut File, 59 | comment: &str, 60 | data: &C, 61 | ) -> Result<(), Box> { 62 | let contents = data.to_file_encoding(comment); 63 | file.write_all(&contents)?; 64 | 65 | Ok(()) 66 | } 67 | 68 | // Annoyingly `p.with_extension("sig")` will turn `foo.bar` into `foo.sig`. This 69 | // avoids that issue (and without requiring the path be UTF-8), but is kind of 70 | // tedious. Note that `ext` should be something like `"sig"` and not `".sig"`. 71 | fn add_extension(p: impl Into, ext: &str) -> PathBuf { 72 | use std::ffi::OsString; 73 | let mut path: PathBuf = p.into(); 74 | let mut name: OsString = path.file_name().unwrap_or_default().to_owned(); 75 | name.push("."); 76 | name.push(ext); 77 | path.set_file_name(name); 78 | path 79 | } 80 | 81 | #[test] 82 | fn test_add_extension() { 83 | #[track_caller] 84 | fn check(p: impl AsRef, e: &str, want: impl AsRef) { 85 | let p = p.as_ref(); 86 | let want = want.as_ref(); 87 | assert_eq!(add_extension(p, e), want); 88 | } 89 | check("foo", "sig", "foo.sig"); 90 | check("foo.bar.baz", "sig", "foo.bar.baz.sig"); 91 | check("/a/b/c/foo.bar.baz", "sig", "/a/b/c/foo.bar.baz.sig"); 92 | check("/a/b/c/foo", "sig", "/a/b/c/foo.sig"); 93 | 94 | check("foo.bar.baz", "a.b", "foo.bar.baz.a.b"); 95 | check("foo", "a.b", "foo.a.b"); 96 | check("/a/b/c/foo.bar.baz", "a.b", "/a/b/c/foo.bar.baz.a.b"); 97 | check("/a/b/c/foo", "a.b", "/a/b/c/foo.a.b"); 98 | } 99 | 100 | fn read_base64_file( 101 | reader: &mut BufReader, 102 | ) -> Result<(C, u64), Box> { 103 | let mut contents = String::with_capacity(1024); 104 | // Optimization: Read the two lines that have the comment and well structured data 105 | // instead of the entire file in case the message was large and embedded. 106 | reader.read_line(&mut contents)?; 107 | reader.read_line(&mut contents)?; 108 | 109 | let read = C::from_base64(&contents)?; 110 | Ok(read) 111 | } 112 | 113 | fn verify( 114 | pubkey_path: &Path, 115 | msg_path: &Path, 116 | signature_path: Option, 117 | embed: bool, 118 | ) -> Result<(), Box> { 119 | let mut pubkey_file = BufReader::new(File::open(pubkey_path)?); 120 | let public_key: PublicKey = read_base64_file(&mut pubkey_file)?.0; 121 | 122 | let signature_path = match signature_path { 123 | Some(path) => path, 124 | None => add_extension(msg_path, "sig"), 125 | }; 126 | 127 | let mut sig_data = BufReader::new(File::open(signature_path)?); 128 | 129 | let (signature, bytes_read): (Signature, u64) = read_base64_file(&mut sig_data)?; 130 | 131 | let mut msg = vec![]; 132 | 133 | if embed { 134 | // Jump around past the well structured signify message to the 135 | // embedded contents. 136 | sig_data.seek(SeekFrom::Start(bytes_read))?; 137 | sig_data.read_to_end(&mut msg)?; 138 | } else { 139 | let mut msg_file = File::open(msg_path)?; 140 | msg_file.read_to_end(&mut msg)?; 141 | } 142 | 143 | public_key 144 | .verify(&msg, &signature) 145 | .map(|_| { 146 | println!("Signature Verified"); 147 | }) 148 | .map_err(Into::into) 149 | } 150 | 151 | fn sign( 152 | private_key_path: &Path, 153 | msg_path: &Path, 154 | signature_path: Option, 155 | embed: bool, 156 | ) -> Result<(), Box> { 157 | let mut secret_key = BufReader::new(File::open(private_key_path)?); 158 | let mut secret_key: PrivateKey = read_base64_file(&mut secret_key)?.0; 159 | 160 | if secret_key.is_encrypted() { 161 | let passphrase = read_passphrase(false)?; 162 | secret_key.decrypt_with_password(&passphrase)?; 163 | } 164 | 165 | let mut msg_file = File::open(msg_path)?; 166 | let mut msg = vec![]; 167 | msg_file.read_to_end(&mut msg)?; 168 | 169 | let signature_path = match signature_path { 170 | Some(path) => path, 171 | None => add_extension(msg_path, "sig"), 172 | }; 173 | 174 | let sig = secret_key.sign(&msg); 175 | 176 | let sig_comment = "signature from signify secret key"; 177 | 178 | let mut file = OpenOptions::new() 179 | .write(true) 180 | .create_new(true) 181 | .open(signature_path)?; 182 | 183 | write_base64_file(&mut file, sig_comment, &sig)?; 184 | 185 | if embed { 186 | file.write_all(&msg)?; 187 | } 188 | 189 | Ok(()) 190 | } 191 | 192 | fn read_passphrase(confirm: bool) -> Result> { 193 | let passphrase = rpassword::prompt_password("passphrase: ")?; 194 | 195 | if confirm { 196 | let confirm_passphrase = rpassword::prompt_password("confirm passphrase: ")?; 197 | 198 | if passphrase != confirm_passphrase { 199 | return Err("passwords don't match".into()); 200 | } 201 | } 202 | 203 | Ok(passphrase) 204 | } 205 | 206 | fn generate( 207 | pubkey_path: &Path, 208 | privkey_path: &Path, 209 | comment: Option<&str>, 210 | kdfrounds: Option, 211 | ) -> Result<(), Box> { 212 | let comment = comment.unwrap_or("signify"); 213 | 214 | let derivation_info = match kdfrounds { 215 | Some(kdf_rounds) => { 216 | let passphrase = read_passphrase(true)?; 217 | NewKeyOpts::Encrypted { 218 | passphrase, 219 | kdf_rounds, 220 | } 221 | } 222 | None => NewKeyOpts::NoEncryption, 223 | }; 224 | 225 | // Store the private key 226 | let mut rng = rand_core::OsRng {}; 227 | let private_key = PrivateKey::generate(&mut rng, derivation_info)?; 228 | 229 | let priv_comment = format!("{} secret key", comment); 230 | let mut file = OpenOptions::new() 231 | .write(true) 232 | .create_new(true) 233 | .open(privkey_path)?; 234 | 235 | write_base64_file(&mut file, &priv_comment, &private_key)?; 236 | 237 | // Store public key 238 | let public_key = private_key.public(); 239 | 240 | let pub_comment = format!("{} public key", comment); 241 | let mut file = OpenOptions::new() 242 | .write(true) 243 | .create_new(true) 244 | .open(pubkey_path)?; 245 | 246 | write_base64_file(&mut file, &pub_comment, &public_key)?; 247 | 248 | Ok(()) 249 | } 250 | 251 | fn human(res: Result>) -> T { 252 | match res { 253 | Ok(val) => val, 254 | Err(e) => { 255 | println!("error: {}", e); 256 | process::exit(1); 257 | } 258 | } 259 | } 260 | 261 | fn unwrap_path(kind: &'static str, path: Option) -> T { 262 | match path { 263 | Some(p) => p, 264 | None => { 265 | println!("missing path to {}", kind); 266 | process::exit(1) 267 | } 268 | } 269 | } 270 | 271 | fn main() { 272 | let args = Args::parse(); 273 | 274 | if args.verify { 275 | let public_key = unwrap_path("pubkey", args.pubkey); 276 | let message = unwrap_path("message", args.message_path); 277 | 278 | human(verify( 279 | &public_key, 280 | &message, 281 | args.signature_path, 282 | args.embed_message, 283 | )); 284 | 285 | return; 286 | } 287 | 288 | if args.generate { 289 | let public_key = unwrap_path("pubkey", args.pubkey); 290 | let private_key = unwrap_path("seckey", args.seckey); 291 | let rounds = if args.skip_key_encryption { 292 | None 293 | } else { 294 | Some(DEFAULT_KDF_ROUNDS) 295 | }; 296 | 297 | human(generate( 298 | &public_key, 299 | &private_key, 300 | args.comment.as_deref(), 301 | rounds, 302 | )); 303 | 304 | return; 305 | } 306 | 307 | if args.sign { 308 | let private_key = unwrap_path("seckey", args.seckey); 309 | let msg_path = unwrap_path("message", args.message_path); 310 | 311 | human(sign( 312 | &private_key, 313 | &msg_path, 314 | args.signature_path, 315 | args.embed_message, 316 | )); 317 | 318 | return; 319 | } 320 | 321 | // No command specified. 322 | println!("{}", ::command().render_usage()); 323 | } 324 | -------------------------------------------------------------------------------- /signify/tests/compare.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | pushd $(dirname $0) > /dev/null 6 | SCRIPTPATH=$(pwd) 7 | popd > /dev/null 8 | cd $SCRIPTPATH 9 | 10 | SIGNIFY_REPO="https://github.com/aperezdc/signify" 11 | 12 | MSG=$(mktemp -u msg.$$.XXXXXXXXXX) 13 | PUB="${MSG}_key.pub" 14 | PRIV="${MSG}_key.sec" 15 | 16 | cleanup() { 17 | rm -f $PUB $PRIV $MSG $MSG.sig || true 18 | } 19 | 20 | cargo_run() { 21 | cargo run -q $BUILD_MODE -- $* 22 | } 23 | 24 | trap cleanup SIGHUP SIGINT SIGTERM EXIT 25 | 26 | git clone $SIGNIFY_REPO || true 27 | pushd signify 28 | git reset --hard 29 | git pull 30 | git submodule update --init 31 | rm -rf libbsd-* 32 | make clean all \ 33 | BUNDLED_LIBBSD=1 \ 34 | BUNDLED_LIBBSD_VERIFY_GPG=0 \ 35 | LIBBSD_LDFLAGS="-lrt" \ 36 | WGET="wget --no-check-certificate" 37 | popd 38 | 39 | SIGNIFY=$(pwd)/signify/signify 40 | 41 | echo "==> Testing Rust Generate/Sign, C Verify" 42 | cargo_run -G -n -p $PUB -s $PRIV 43 | head -c 100 /dev/urandom > $MSG 44 | cargo_run -S -s $PRIV -m $MSG -x ${MSG}.sig 45 | $SIGNIFY -V -p $PUB -m $MSG -x ${MSG}.sig 46 | cleanup 47 | 48 | echo "==> Testing Rust Generate, C Sign/Verify" 49 | cargo_run -G -n -p $PUB -s $PRIV 50 | head -c 100 /dev/urandom > $MSG 51 | $SIGNIFY -S -s $PRIV -m $MSG -x ${MSG}.sig 52 | $SIGNIFY -V -p $PUB -m $MSG -x ${MSG}.sig 53 | cleanup 54 | 55 | echo "==> Testing Rust Generate, C Sign, Rust Verify" 56 | cargo_run -G -n -p $PUB -s $PRIV 57 | head -c 100 /dev/urandom > $MSG 58 | $SIGNIFY -S -s $PRIV -m $MSG -x ${MSG}.sig 59 | cargo_run -V -p $PUB -m $MSG -x ${MSG}.sig 60 | cleanup 61 | 62 | echo "==> Testing C Generate/Sign, Rust Verify" 63 | $SIGNIFY -G -n -p $PUB -s $PRIV 64 | head -c 100 /dev/urandom > $MSG 65 | $SIGNIFY -S -s $PRIV -m $MSG -x ${MSG}.sig 66 | cargo_run -V -p $PUB -m $MSG -x ${MSG}.sig 67 | cleanup 68 | 69 | echo "==> Testing C Generate, Rust Sign/Verify" 70 | $SIGNIFY -G -n -p $PUB -s $PRIV 71 | head -c 100 /dev/urandom > $MSG 72 | cargo_run -S -s $PRIV -m $MSG -x ${MSG}.sig 73 | cargo_run -V -p $PUB -m $MSG -x ${MSG}.sig 74 | cleanup 75 | 76 | echo "==> Testing C Generate, Rust Sign, C Verify" 77 | $SIGNIFY -G -n -p $PUB -s $PRIV 78 | head -c 100 /dev/urandom > $MSG 79 | cargo_run -S -s $PRIV -m $MSG -x ${MSG}.sig 80 | $SIGNIFY -V -p $PUB -m $MSG -x ${MSG}.sig 81 | cleanup 82 | 83 | printf "\n\033[1;37m\\o/ \033[0;32mAll tests passed without errors!\033[0m\n" 84 | 85 | exit 0 86 | -------------------------------------------------------------------------------- /signify/tests/full-cycle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | pushd $(dirname $0) > /dev/null 6 | SCRIPTPATH=$(pwd) 7 | popd > /dev/null 8 | cd $SCRIPTPATH 9 | 10 | PUB=$(mktemp -u pub.$$.XXXXXXXXXX) 11 | PRIV=$(mktemp -u priv.$$.XXXXXXXXXX) 12 | MSG=$(mktemp -u msg.$$.XXXXXXXXXX) 13 | 14 | cleanup() { 15 | rm -f $PUB $PRIV $MSG $MSG.sig || true 16 | } 17 | 18 | cargo_run() { 19 | cargo run -q $BUILD_MODE -- $* 20 | } 21 | 22 | trap cleanup SIGHUP SIGINT SIGTERM EXIT 23 | 24 | echo "==> Normal Cycle" 25 | cargo_run -G -n -p $PUB -s $PRIV 26 | head -c 100 /dev/urandom > $MSG 27 | cargo_run -S -s $PRIV -m $MSG -x ${MSG}.sig 28 | cargo_run -V -p $PUB -m $MSG -x ${MSG}.sig 29 | cleanup 30 | 31 | echo "==> Cycle (embedded)" 32 | cargo_run -G -n -p $PUB -s $PRIV 33 | head -c 100 /dev/urandom > $MSG 34 | cargo_run -S -e -s $PRIV -m $MSG 35 | cargo_run -V -e -p $PUB -m $MSG 36 | 37 | MSG_SIZE=$(wc -c $MSG | awk '{print $1}') 38 | SIG_SIZE=$(wc -c $MSG.sig | awk '{print $1}') 39 | [ $SIG_SIZE -gt $MSG_SIZE ] 40 | 41 | cleanup 42 | 43 | printf "\n\033[1;37m\\o/ \033[0;32mAll tests passed without errors!\033[0m\n" 44 | 45 | exit 0 46 | -------------------------------------------------------------------------------- /signify/tests/integration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | pushd $(dirname $0) > /dev/null 6 | SCRIPTPATH=$(pwd) 7 | popd > /dev/null 8 | cd $SCRIPTPATH 9 | 10 | PUB=$(mktemp -u pub.$$.XXXXXXXXXX) 11 | PRIV=$(mktemp -u priv.$$.XXXXXXXXXX) 12 | MSG=$(mktemp -u msg.$$.XXXXXXXXXX) 13 | 14 | cleanup() { 15 | rm -f $PUB $PRIV $MSG $MSG.sig || true 16 | } 17 | 18 | cargo_run() { 19 | cargo run -q $BUILD_MODE -- $* 20 | } 21 | 22 | assert_false() { 23 | set +e 24 | $* 25 | res=$? 26 | set -e 27 | [ $res -ne 0 ] 28 | } 29 | 30 | trap cleanup SIGHUP SIGINT SIGTERM EXIT 31 | 32 | echo "==> Message modified" 33 | cargo_run -G -n -p $PUB -s $PRIV 34 | head -c 100 /dev/urandom > $MSG 35 | cargo_run -S -s $PRIV -m $MSG -x ${MSG}.sig 36 | head -c 1 /dev/urandom >> $MSG 37 | assert_false cargo_run -V -p $PUB -m $MSG -x ${MSG}.sig 38 | cp ${MSG}.sig ${MSG}.sig.2 39 | cleanup 40 | 41 | echo "==> Signature modified" 42 | cargo_run -G -n -p $PUB -s $PRIV 43 | head -c 100 /dev/urandom > $MSG 44 | cargo_run -S -s $PRIV -m $MSG -x ${MSG}.sig 45 | mv ${MSG}.sig.2 ${MSG}.sig 46 | assert_false cargo_run -V -p $PUB -m $MSG -x ${MSG}.sig 47 | cleanup 48 | 49 | echo "==> Embedded Message modified" 50 | cargo_run -G -n -p $PUB -s $PRIV 51 | head -c 100 /dev/urandom > $MSG 52 | cargo_run -S -e -s $PRIV -m $MSG 53 | rm $MSG 54 | head -c 1 /dev/urandom >> ${MSG}.sig 55 | assert_false cargo_run -V -e -p $PUB -m $MSG 56 | cleanup 57 | 58 | printf "\n\033[1;37m\\o/ \033[0;32mAll tests passed without errors!\033[0m\n" 59 | 60 | exit 0 61 | --------------------------------------------------------------------------------