feat(core): RDP client on IronRDP
clients::rdp connects over any caller-dialled TCP stream (direct, proxy or SSH tunnel), upgrades to TLS and logs on with NLA (CredSSP/NTLM), optionally falling back to TLS-only logon for servers without NLA such as xrdp (credentials are passed so they log straight in). The server certificate is pinned through a caller check on its SHA256 fingerprint before any credentials are sent. The desktop decodes into the VNC client's RGBA framebuffer; input (IronRDP's scancode/Unicode/mouse operations), Display Control resizes and the deactivation-reactivation sequence run on the session thread. IronRDP's releases need three vendored patches (vendor/README.md): picky and sspi with their RustCrypto release-candidate pins relaxed so they share russh's stable versions (their own suites, 402 tests, pass against them), and ironrdp-session with two unreleased upstream bitmap fixes (#1486 stride, #1524 RDP6 orientation) without which xrdp desktops render sheared. Verified live against xrdp (scottyhardy/docker-remote-desktop): logon, full desktop rendering, keyboard and mouse input.
This commit is contained in:
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
# Vendored crates
|
||||
|
||||
Patched copies used through `[patch.crates-io]` in `rust/Cargo.toml`. Remove each once an
|
||||
IronRDP release includes the fix it carries.
|
||||
|
||||
| Crate | Upstream | Change |
|
||||
|---|---|---|
|
||||
| `picky` | 7.0.0-rc.25 (crates.io) | `=x.y.z-rc.N` RustCrypto pins relaxed to `^x.y.z-rc.N` (so the stable releases russh uses satisfy them); one unused import removed. |
|
||||
| `sspi` | 0.21.3 (crates.io) | Same pin relaxation. |
|
||||
| `ironrdp-session` | 0.11.0 (crates.io) | Upstream fixes not yet released: #1486 "preserve bitmap source stride" (commit `80bb81b344`, applied as-is) and the RDP6 orientation flip from #1524 (`8f1737a287`). Without them xrdp desktops render sheared and upside-down tile by tile. |
|
||||
|
||||
Why: IronRDP pins `picky = "=7.0.0-rc.25"`, which pins `curve25519-dalek = "=5.0.0-rc.1"` and
|
||||
other release candidates; russh needs the stable `curve25519-dalek 5`, and Cargo can't build two
|
||||
semver-compatible versions of one crate. With the pins relaxed both resolve to the stable
|
||||
releases, and the `picky` and `sspi` unit suites (402 tests) pass against them.
|
||||
|
||||
The `ironrdp-session` fixes are verified by the live test in `core/src/clients/rdp.rs`
|
||||
(`NEXUS_RDP_TEST="host port user password"`), run against xrdp; the published crate ships no
|
||||
unit tests.
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [[0.11.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.10.0...ironrdp-session-v0.11.0)] - 2026-07-10
|
||||
|
||||
### <!-- 0 -->Security
|
||||
|
||||
- [**breaking**] Send NetworkAutoDetect over the MCS message channel ([#1348](https://github.com/Devolutions/IronRDP/issues/1348)) ([8a1fd0118e](https://github.com/Devolutions/IronRDP/commit/8a1fd0118e0bac214c9050b6ca6b36a040046dd3))
|
||||
|
||||
Corrects Network Auto-Detect framing and routing to match MS-RDPBCGR by
|
||||
moving it off the I/O channel slow-path Share Data PDUs and onto the MCS
|
||||
message channel with the required Basic Security Header
|
||||
(SEC_AUTODETECT_REQ / SEC_AUTODETECT_RSP). This aligns IronRDP with
|
||||
mstsc/xfreerdp behavior and enables both connect-time and continuous
|
||||
auto-detection to actually function.
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Propagate caller location through error constructor helpers ([#1392](https://github.com/Devolutions/IronRDP/issues/1392)) ([d6990d81a1](https://github.com/Devolutions/IronRDP/commit/d6990d81a17e8349e52768ad8a82f673b1e1462d))
|
||||
|
||||
The error constructor helpers in several crates wrap the #[track_caller]
|
||||
ironrdp_error::Error::new, but were not themselves marked
|
||||
#[track_caller]. As a result, the captured location pointed at the
|
||||
helper body instead of the real call site, giving misleading "@
|
||||
file:line" info in error reports.
|
||||
|
||||
- Reduce dependency on ironrdp-connector ([#1419](https://github.com/Devolutions/IronRDP/issues/1419)) ([5c22f86a71](https://github.com/Devolutions/IronRDP/commit/5c22f86a7150bc10c26a3be39bfaebf84c67d781))
|
||||
|
||||
Drops session's reliance on ironrdp-connector legacy helpers, now sourced from ironrdp-pdu.
|
||||
|
||||
- [**breaking**] Remove ironrdp-connector dependency ([#1435](https://github.com/Devolutions/IronRDP/issues/1435)) ([c6a0286dcb](https://github.com/Devolutions/IronRDP/commit/c6a0286dcb49d9ac54c65c4f9325b41e05d541b8))
|
||||
|
||||
Removes the last ironrdp-connector coupling from ironrdp-session by
|
||||
turning Deactivate-All handling into a bare signal and shifting ownership
|
||||
of the Deactivation-Reactivation activation sequence back to each consumer.
|
||||
It introduces a ConnectionActivationFactory (fresh sequence per reactivation)
|
||||
and an ActiveStageBuilder so session construction no longer depends on
|
||||
ConnectionResult.
|
||||
|
||||
|
||||
|
||||
## [[0.10.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.9.0...ironrdp-session-v0.10.0)] - 2026-06-05
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Decode RGBA QOI bitmaps instead of dropping the frame ([#1341](https://github.com/Devolutions/IronRDP/issues/1341)) ([ef20ea4e90](https://github.com/Devolutions/IronRDP/commit/ef20ea4e90455d6c6db0d3521f6522d1e960c0bb))
|
||||
|
||||
Fixes the client-side QOI decode path in ironrdp-session so RGBA-channel QOI frames are decoded and applied to the framebuffer instead of being dropped, improving interoperability with third-party RDP servers and older ironrdp-server builds that emit RGBA QOI.
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- [**breaking**] Update `ironrdp-dvc` public dependency
|
||||
|
||||
|
||||
|
||||
## [[0.9.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.8.0...ironrdp-session-v0.9.0)] - 2026-05-27
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- Dispatch multitransport PDUs on IO channel ([#1096](https://github.com/Devolutions/IronRDP/issues/1096)) ([7853e3cc6f](https://github.com/Devolutions/IronRDP/commit/7853e3cc6f26acaf3da000c6177ca3cef6ef85fd))
|
||||
|
||||
`decode_io_channel()` assumes all IO channel PDUs begin with
|
||||
a`ShareControlHeader`. Multitransport Request PDUs use a
|
||||
`BasicSecurityHeader` with `SEC_TRANSPORT_REQ` instead ([MS-RDPBCGR]
|
||||
2.2.15.1).
|
||||
|
||||
This adds a peek-based dispatch: check the first `u16`
|
||||
for`TRANSPORT_REQ`, decode as `MultitransportRequestPdu` if set,
|
||||
otherwise fall through to the existing `decode_share_control()` path
|
||||
unchanged.
|
||||
|
||||
The new variant is propagated through `ProcessorOutput` and
|
||||
'ActiveStageOutput` so applications can handle multitransport requests.
|
||||
Client and web consumers log the request (no UDP transport yet).
|
||||
|
||||
- Add bulk compression and wire negotiation ([ebf5da5f33](https://github.com/Devolutions/IronRDP/commit/ebf5da5f3380a3355f6c95814d669f8190425ded))
|
||||
|
||||
- add ironrdp-bulk crate with MPPC/NCRUSH/XCRUSH, bitstream, benches, and metrics
|
||||
- advertise compression in Client Info and plumb compression_type through connector
|
||||
- decode compressed FastPath/ShareData updates using BulkCompressor
|
||||
- update CLI to numeric compression flags (enabled by default, level 0-3)
|
||||
- extend screenshot example with compression options and negotiated logging
|
||||
- refresh tests, FFI/web configs, typos, and Cargo.lock
|
||||
|
||||
- Complete pixel format support for bitmap updates ([#1134](https://github.com/Devolutions/IronRDP/issues/1134)) ([a6b41093ce](https://github.com/Devolutions/IronRDP/commit/a6b41093ce4ece081d2538c157f6bc547c3b2607))
|
||||
|
||||
Wires missing bitmap pixel formats (8/15/24bpp) into the session rendering
|
||||
pipeline so bitmap updates at those depths are rendered instead of being
|
||||
dropped, and adds fast-path palette update parsing to support 8bpp indexed
|
||||
color sessions.
|
||||
|
||||
- Handle Auto-Detect Request PDUs from server ([#1178](https://github.com/Devolutions/IronRDP/issues/1178)) ([4dcad09980](https://github.com/Devolutions/IronRDP/commit/4dcad09980e4f5354e4e435a134cc0956e2fcf9e))
|
||||
|
||||
Fixes a crash when the server sends Auto-Detect Request PDUs during an
|
||||
active session. After #1176 added ShareDataPdu::AutoDetectReq routing,
|
||||
these PDUs decode correctly but hit the catch-all error path in the x224
|
||||
processor: "unhandled PDU: Auto-Detect Request PDU".
|
||||
|
||||
- Handle slow-path graphics and pointer updates ([#1132](https://github.com/Devolutions/IronRDP/issues/1132)) ([9383380292](https://github.com/Devolutions/IronRDP/commit/938338029290f1be82a7f784d544bb77ac797aeb))
|
||||
|
||||
Adds support for slow-path graphics and pointer updates to IronRDP, fixing connectivity issues with servers like XRDP that use slow-path output instead of fast-path. The implementation parses slow-path framing headers and routes the inner payload structures through the existing fast-path processing pipeline by extracting shared bitmap and pointer processing methods.
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Fix pixel format handling in bitmap decoders ([#1101](https://github.com/Devolutions/IronRDP/issues/1101)) ([75863245ab](https://github.com/Devolutions/IronRDP/commit/75863245ab376f15e35c00df434860c93b123633))
|
||||
|
||||
- Handle row padding in uncompressed bitmap updates ([4262ae75ff](https://github.com/Devolutions/IronRDP/commit/4262ae75ffa5cb1fabb4ca07d598e33d855e8fdd))
|
||||
|
||||
Uncompressed bitmap data has rows padded to 4-byte boundaries per
|
||||
[MS-RDPBCGR] 2.2.9.1.1.3.1.2.2, but the bitmap apply functions
|
||||
expect tightly packed pixel data. Strip the per-row padding before
|
||||
passing raw bitmap data to the apply functions.
|
||||
|
||||
This fixes garbled bitmap rendering when connecting to servers that
|
||||
send uncompressed bitmaps with non-aligned row widths, such as XRDP
|
||||
at 16 bpp.
|
||||
|
||||
- Skip bitmap updates that exceed bounds ([#1146](https://github.com/Devolutions/IronRDP/issues/1146)) ([2b97a95e6d](https://github.com/Devolutions/IronRDP/commit/2b97a95e6da8833e8a84e9f42960da91eee87cd6))
|
||||
|
||||
After a desktop resize, an RDP server can send a burst of bitmap updates
|
||||
for the old resolution before its rendering pipeline has fully
|
||||
transitioned to the new one. These updates reference coordinates beyond
|
||||
the current image buffer in `DecodedImage`, causing index-out-of-bounds
|
||||
panics in the `apply_*` methods. On the server side, the same stale
|
||||
bitmaps can reach the encoder with dimensions exceeding the negotiated
|
||||
desktop size, panicking in `NoneHandler::handle()`.
|
||||
|
||||
This commit adds bounds checks at two levels:
|
||||
- `DecodedImage::rect_fits()` guard at the entry of each `apply_*`
|
||||
method, returning an empty rectangle when the update doesn't fit
|
||||
- Encoder-level guard in `EncoderIter::next()` that drops
|
||||
`BitmapUpdate`s exceeding the current desktop size
|
||||
|
||||
- Propagate negotiated share_id to all outgoing ShareDataPdu ([#1147](https://github.com/Devolutions/IronRDP/issues/1147)) ([2b24e9664d](https://github.com/Devolutions/IronRDP/commit/2b24e9664dd05620ff63a24d092377477fdde863))
|
||||
|
||||
## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.7.0...ironrdp-session-v0.8.0)] - 2025-12-18
|
||||
|
||||
|
||||
## [[0.6.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.5.0...ironrdp-session-v0.6.0)] - 2025-08-29
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- Add QOI image codec ([613fd51f26](https://github.com/Devolutions/IronRDP/commit/613fd51f26315d8212662c46f8e625c541e4bb59))
|
||||
|
||||
The Quite OK Image format ([1]) losslessly compresses images to a similar size
|
||||
of PNG, while offering 20x-50x faster encoding and 3x-4x faster decoding.
|
||||
|
||||
- Add QOIZ image codec ([87df67fdc7](https://github.com/Devolutions/IronRDP/commit/87df67fdc76ff4f39d4b83521e34bf3b5e2e73bb))
|
||||
|
||||
Add a new QOIZ codec for SetSurface command. The PDU data contains the same
|
||||
data as the QOI codec, with zstd compression.
|
||||
|
||||
## [[0.4.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.4.0...ironrdp-session-v0.4.1)] - 2025-06-27
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- More functions on `ActiveStage` (#791) ([5482365655](https://github.com/Devolutions/IronRDP/commit/5482365655e5c171cd967eda401b01161a9f6602))
|
||||
- `get_dvc_by_channel_id`
|
||||
- `encode_dvc_messages`
|
||||
|
||||
|
||||
## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.3.0...ironrdp-session-v0.4.0)] - 2025-05-27
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- [**breaking**] Make DecodedImage Send ([45f66117ba](https://github.com/Devolutions/IronRDP/commit/45f66117ba05170d95b21ec7d97017b44b954f28))
|
||||
|
||||
- Add DecodeImage helpers ([cd7a60ba45](https://github.com/Devolutions/IronRDP/commit/cd7a60ba45a0241be4ecf3860ec4f82b431a7ce2))
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Update rectangle when applying None codecs updates (#728) ([a50cd643dc](https://github.com/Devolutions/IronRDP/commit/a50cd643dce9621f314231b7598d2fd31e4718c6))
|
||||
|
||||
- Return the correct updated region ([7507a152f1](https://github.com/Devolutions/IronRDP/commit/7507a152f14db594e4067bbc01e243cfba77770f))
|
||||
|
||||
"update_rectangle" is set to empty(). The surface updates are then added
|
||||
by "union". But a union with an empty rectangle at (0,0) is still a
|
||||
rectangle at (0,0). We end up with big region updates rooted at (0,0)...
|
||||
|
||||
- Decrease verbosity of Rfx frame_index ([b31b99eafb](https://github.com/Devolutions/IronRDP/commit/b31b99eafb0aac2a5e5a610af21a4027ae5cd698))
|
||||
|
||||
- Decrease verbosity of FastPath header ([f9b6992e74](https://github.com/Devolutions/IronRDP/commit/f9b6992e74abb929f3001e76abaff5d7215e1cb4))
|
||||
|
||||
|
||||
## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.2.3...ironrdp-session-v0.3.0)] - 2025-03-12
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Bump ironrdp-pdu
|
||||
|
||||
## [[0.2.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.2.2...ironrdp-session-v0.2.3)] - 2025-03-12
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa))
|
||||
|
||||
|
||||
## [[0.2.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.2.1...ironrdp-session-v0.2.2)] - 2025-01-28
|
||||
|
||||
### <!-- 6 -->Documentation
|
||||
|
||||
- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b))
|
||||
|
||||
|
||||
## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.2.0...ironrdp-session-v0.2.1)] - 2024-12-14
|
||||
|
||||
### Other
|
||||
|
||||
- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a))
|
||||
+735
@@ -0,0 +1,735 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8"
|
||||
dependencies = [
|
||||
"asn1-rs-derive",
|
||||
"asn1-rs-impl",
|
||||
"displaydoc",
|
||||
"nom",
|
||||
"num-traits",
|
||||
"rusticata-macros",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs-derive"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs-impl"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837"
|
||||
dependencies = [
|
||||
"funty",
|
||||
"radium",
|
||||
"tap",
|
||||
"wyz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"der_derive",
|
||||
"flagset",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der-parser"
|
||||
version = "10.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
"displaydoc",
|
||||
"nom",
|
||||
"num-traits",
|
||||
"rusticata-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der_derive"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "flagset"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe"
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-bulk"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e548d9fd162558a5a8aaed72e528556ce88e77773634e3722b8d01d6f170388"
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-core"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef0875b98275068b88652e49ba2e0a1150a1390aad69d26c942c8c59e8ec918e"
|
||||
dependencies = [
|
||||
"ironrdp-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-displaycontrol"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74a111a6fd25abbe51b6a15276fe980c6b9eee50a1421224b0c3f45848d45bcf"
|
||||
dependencies = [
|
||||
"ironrdp-core",
|
||||
"ironrdp-dvc",
|
||||
"ironrdp-pdu",
|
||||
"ironrdp-svc",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-dvc"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a5de64988ddabf96928e2f042e1772a5f7201f0001ea99012129c73a105fc52"
|
||||
dependencies = [
|
||||
"ironrdp-core",
|
||||
"ironrdp-pdu",
|
||||
"ironrdp-svc",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-error"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd344ce9518ab83f6f7568ca4f1bc6dc8c55bd2da04cb5ee7b3e8740d5041734"
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-graphics"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7493e426b6a8104cd497e518ba7781a9c7fc9a5f58a9cc0c1111e6d66f63bcc"
|
||||
dependencies = [
|
||||
"bit_field",
|
||||
"bitflags",
|
||||
"bitvec",
|
||||
"byteorder",
|
||||
"ironrdp-core",
|
||||
"ironrdp-pdu",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"yuv",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-pdu"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ccd1179a4d106df1930347701388b5c79bc3725fa7dab4438d57db0d9d19347"
|
||||
dependencies = [
|
||||
"bit_field",
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"der-parser",
|
||||
"ironrdp-core",
|
||||
"ironrdp-error",
|
||||
"md-5",
|
||||
"num-bigint",
|
||||
"num-derive",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"pkcs1",
|
||||
"sha1",
|
||||
"tap",
|
||||
"x509-cert",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-session"
|
||||
version = "0.11.0"
|
||||
dependencies = [
|
||||
"ironrdp-bulk",
|
||||
"ironrdp-core",
|
||||
"ironrdp-displaycontrol",
|
||||
"ironrdp-dvc",
|
||||
"ironrdp-error",
|
||||
"ironrdp-graphics",
|
||||
"ironrdp-pdu",
|
||||
"ironrdp-svc",
|
||||
"qoicoubeh",
|
||||
"tracing",
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-svc"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24c36b82ab0f7fef2668fb7004008a0f3c100a3d9a7b18bd4495a71aba55b796"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ironrdp-core",
|
||||
"ironrdp-pdu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkcs1"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
|
||||
dependencies = [
|
||||
"der",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qoicoubeh"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9b82aa3fef8a980075775b8c46f874823b5b4a15de327d2dbb3b6fd818480ba"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||
|
||||
[[package]]
|
||||
name = "rusticata-macros"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tap"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tls_codec"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b"
|
||||
dependencies = [
|
||||
"tls_codec_derive",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tls_codec_derive"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wyz"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
|
||||
dependencies = [
|
||||
"tap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x509-cert"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"der",
|
||||
"spki",
|
||||
"tls_codec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yuv"
|
||||
version = "0.8.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d85a782d94ee43f078bcfd6fa82d4e6a5b2d1cfbbad168e4df5a9f7b39ef48c"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.16+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2024"
|
||||
rust-version = "1.89"
|
||||
name = "ironrdp-session"
|
||||
version = "0.11.0"
|
||||
authors = [
|
||||
"Devolutions Inc. <infos@devolutions.net>",
|
||||
"Teleport <goteleport.com>",
|
||||
]
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "State machines to drive an RDP session"
|
||||
homepage = "https://github.com/Devolutions/IronRDP"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"rdp",
|
||||
"remote-desktop",
|
||||
"network",
|
||||
"client",
|
||||
"protocol",
|
||||
]
|
||||
categories = ["network-programming"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/Devolutions/IronRDP"
|
||||
resolver = "2"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
qoi = [
|
||||
"dep:qoicoubeh",
|
||||
"ironrdp-pdu/qoi",
|
||||
]
|
||||
qoiz = [
|
||||
"dep:zstd-safe",
|
||||
"qoi",
|
||||
]
|
||||
|
||||
[lib]
|
||||
name = "ironrdp_session"
|
||||
path = "src/lib.rs"
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
[dependencies.ironrdp-bulk]
|
||||
version = "0.1"
|
||||
|
||||
[dependencies.ironrdp-core]
|
||||
version = "0.2"
|
||||
|
||||
[dependencies.ironrdp-displaycontrol]
|
||||
version = "0.8"
|
||||
|
||||
[dependencies.ironrdp-dvc]
|
||||
version = "0.8"
|
||||
|
||||
[dependencies.ironrdp-error]
|
||||
version = "0.2"
|
||||
|
||||
[dependencies.ironrdp-graphics]
|
||||
version = "0.9"
|
||||
|
||||
[dependencies.ironrdp-pdu]
|
||||
version = "0.9"
|
||||
features = ["std"]
|
||||
|
||||
[dependencies.ironrdp-svc]
|
||||
version = "0.8"
|
||||
|
||||
[dependencies.qoicoubeh]
|
||||
version = "0.5"
|
||||
optional = true
|
||||
|
||||
[dependencies.tracing]
|
||||
version = "0.1"
|
||||
features = ["log"]
|
||||
|
||||
[dependencies.zstd-safe]
|
||||
version = "7.2"
|
||||
features = ["std"]
|
||||
optional = true
|
||||
|
||||
[lints.clippy]
|
||||
alloc_instead_of_core = "warn"
|
||||
allow_attributes = "warn"
|
||||
as_conversions = "warn"
|
||||
as_pointer_underscore = "warn"
|
||||
as_ptr_cast_mut = "warn"
|
||||
as_underscore = "warn"
|
||||
cast_lossless = "warn"
|
||||
cast_possible_truncation = "warn"
|
||||
cast_possible_wrap = "warn"
|
||||
cast_ptr_alignment = "warn"
|
||||
cast_sign_loss = "warn"
|
||||
cfg_not_test = "warn"
|
||||
checked_conversions = "warn"
|
||||
clone_on_ref_ptr = "warn"
|
||||
cloned_instead_of_copied = "warn"
|
||||
collection_is_never_read = "warn"
|
||||
copy_iterator = "warn"
|
||||
dbg_macro = "warn"
|
||||
deref_by_slicing = "warn"
|
||||
disallowed_script_idents = "warn"
|
||||
doc_include_without_cfg = "warn"
|
||||
empty_drop = "warn"
|
||||
empty_enum_variants_with_brackets = "warn"
|
||||
expl_impl_clone_on_copy = "warn"
|
||||
filetype_is_file = "warn"
|
||||
float_cmp = "warn"
|
||||
float_cmp_const = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
get_unwrap = "warn"
|
||||
implicit_clone = "warn"
|
||||
infinite_loop = "warn"
|
||||
inline_always = "warn"
|
||||
large_futures = "warn"
|
||||
large_include_file = "warn"
|
||||
large_stack_frames = "warn"
|
||||
large_types_passed_by_value = "warn"
|
||||
lossy_float_literal = "warn"
|
||||
map_with_unused_argument_over_ranges = "warn"
|
||||
mem_forget = "warn"
|
||||
missing_panics_doc = "warn"
|
||||
missing_safety_doc = "warn"
|
||||
mixed_read_write_in_expression = "warn"
|
||||
multiple_inherent_impl = "warn"
|
||||
multiple_unsafe_ops_per_block = "warn"
|
||||
needless_raw_strings = "warn"
|
||||
non_ascii_literal = "warn"
|
||||
non_zero_suggestions = "warn"
|
||||
or_fun_call = "warn"
|
||||
panic = "warn"
|
||||
partial_pub_fields = "warn"
|
||||
precedence_bits = "warn"
|
||||
print_stderr = "warn"
|
||||
print_stdout = "warn"
|
||||
ptr_cast_constness = "warn"
|
||||
pub_without_shorthand = "warn"
|
||||
range_plus_one = "warn"
|
||||
rc_buffer = "warn"
|
||||
rc_mutex = "warn"
|
||||
redundant_clone = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
renamed_function_params = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
return_self_not_must_use = "warn"
|
||||
same_name_method = "warn"
|
||||
self_named_module_files = "warn"
|
||||
semicolon_outside_block = "warn"
|
||||
separated_literal_suffix = "warn"
|
||||
similar_names = "warn"
|
||||
std_instead_of_core = "warn"
|
||||
str_to_string = "warn"
|
||||
string_add = "warn"
|
||||
string_lit_chars_any = "warn"
|
||||
string_slice = "warn"
|
||||
suspicious_xor_used_as_pow = "warn"
|
||||
todo = "warn"
|
||||
trait_duplication_in_bounds = "warn"
|
||||
transmute_ptr_to_ptr = "warn"
|
||||
try_err = "warn"
|
||||
type_repetition_in_bounds = "warn"
|
||||
undocumented_unsafe_blocks = "warn"
|
||||
unnecessary_box_returns = "warn"
|
||||
unnecessary_safety_comment = "warn"
|
||||
unnecessary_self_imports = "warn"
|
||||
unused_result_ok = "warn"
|
||||
unused_self = "warn"
|
||||
unused_trait_names = "warn"
|
||||
unwrap_used = "warn"
|
||||
useless_let_if_seq = "warn"
|
||||
wildcard_dependencies = "warn"
|
||||
wildcard_imports = "warn"
|
||||
|
||||
[lints.rust]
|
||||
absolute_paths_not_starting_with_crate = "warn"
|
||||
ambiguous_negative_literals = "warn"
|
||||
elided_lifetimes_in_paths = "warn"
|
||||
invalid_reference_casting = "warn"
|
||||
keyword_idents = "warn"
|
||||
macro_use_extern_crate = "warn"
|
||||
missing_unsafe_on_extern = "warn"
|
||||
noop_method_call = "warn"
|
||||
redundant_imports = "warn"
|
||||
redundant_lifetimes = "warn"
|
||||
single_use_lifetimes = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
unit_bindings = "warn"
|
||||
unreachable_pub = "warn"
|
||||
unsafe_attr_outside_unsafe = "warn"
|
||||
unsafe_op_in_unsafe_fn = "warn"
|
||||
unused_crate_dependencies = "warn"
|
||||
unused_lifetimes = "warn"
|
||||
unused_macro_rules = "warn"
|
||||
unused_qualifications = "warn"
|
||||
unused_unsafe = "warn"
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
[package]
|
||||
name = "ironrdp-session"
|
||||
version = "0.11.0"
|
||||
readme = "README.md"
|
||||
description = "State machines to drive an RDP session"
|
||||
edition.workspace = true
|
||||
rust-version = "1.89"
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
qoi = ["dep:qoicoubeh", "ironrdp-pdu/qoi"]
|
||||
qoiz = ["dep:zstd-safe", "qoi"]
|
||||
|
||||
[dependencies]
|
||||
ironrdp-bulk = { path = "../ironrdp-bulk", version = "0.1" }
|
||||
ironrdp-core = { path = "../ironrdp-core", version = "0.2" } # public
|
||||
ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public
|
||||
ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8" } # public
|
||||
ironrdp-error = { path = "../ironrdp-error", version = "0.2" } # public
|
||||
ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.9" } # public
|
||||
ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9", features = ["std"] } # public
|
||||
ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.8" }
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
qoicoubeh = { version = "0.5", optional = true }
|
||||
zstd-safe = { version = "7.2", optional = true, features = ["std"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
# IronRDP Session
|
||||
|
||||
Abstract state machine to drive an RDP session.
|
||||
|
||||
This crate is part of the [IronRDP] project.
|
||||
|
||||
[IronRDP]: https://github.com/Devolutions/IronRDP
|
||||
+469
@@ -0,0 +1,469 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ironrdp_bulk::BulkCompressor;
|
||||
use ironrdp_core::{ReadCursor, WriteBuf};
|
||||
use ironrdp_displaycontrol::client::DisplayControlClient;
|
||||
use ironrdp_dvc::{DrdynvcClient, DvcProcessor, DynamicVirtualChannel};
|
||||
use ironrdp_graphics::pointer::DecodedPointer;
|
||||
use ironrdp_pdu::geometry::InclusiveRectangle;
|
||||
use ironrdp_pdu::input::fast_path::{FastPathInput, FastPathInputEvent};
|
||||
use ironrdp_pdu::rdp::autodetect::AutoDetectRequest;
|
||||
use ironrdp_pdu::rdp::client_info::CompressionType as PduCompressionType;
|
||||
use ironrdp_pdu::rdp::headers::ShareDataPdu;
|
||||
use ironrdp_pdu::rdp::multitransport::MultitransportRequestPdu;
|
||||
use ironrdp_pdu::slow_path::{self, GraphicsUpdateType};
|
||||
use ironrdp_pdu::{Action, mcs};
|
||||
use ironrdp_svc::{StaticChannelSet, SvcMessage, SvcProcessor, SvcProcessorMessages};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::fast_path::UpdateKind;
|
||||
use crate::image::DecodedImage;
|
||||
use crate::{SessionError, SessionErrorExt as _, SessionResult, fast_path, x224};
|
||||
|
||||
/// Converts the PDU-layer compression type to the bulk crate's compression type.
|
||||
fn to_bulk_compression_type(ct: PduCompressionType) -> ironrdp_bulk::CompressionType {
|
||||
match ct {
|
||||
PduCompressionType::K8 => ironrdp_bulk::CompressionType::Rdp4,
|
||||
PduCompressionType::K64 => ironrdp_bulk::CompressionType::Rdp5,
|
||||
PduCompressionType::Rdp6 => ironrdp_bulk::CompressionType::Rdp6,
|
||||
PduCompressionType::Rdp61 => ironrdp_bulk::CompressionType::Rdp61,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ActiveStage {
|
||||
x224_processor: x224::Processor,
|
||||
fast_path_processor: fast_path::Processor,
|
||||
enable_server_pointer: bool,
|
||||
}
|
||||
|
||||
/// Builder for [`ActiveStage`].
|
||||
///
|
||||
/// All fields are required; they are typically taken straight from `ironrdp-connector`’s
|
||||
/// `ConnectionResult` once the connection sequence is finalized.
|
||||
pub struct ActiveStageBuilder {
|
||||
pub static_channels: StaticChannelSet,
|
||||
pub user_channel_id: u16,
|
||||
pub io_channel_id: u16,
|
||||
pub message_channel_id: Option<u16>,
|
||||
pub share_id: u32,
|
||||
/// The bulk compression type that was negotiated, if any.
|
||||
pub compression_type: Option<PduCompressionType>,
|
||||
/// Enable server-side pointer updates (client-side pointer rendering).
|
||||
pub enable_server_pointer: bool,
|
||||
/// Use software rendering mode for pointer bitmap generation.
|
||||
pub pointer_software_rendering: bool,
|
||||
}
|
||||
|
||||
impl ActiveStageBuilder {
|
||||
pub fn build(self) -> ActiveStage {
|
||||
let Self {
|
||||
static_channels,
|
||||
user_channel_id,
|
||||
io_channel_id,
|
||||
message_channel_id,
|
||||
share_id,
|
||||
compression_type,
|
||||
enable_server_pointer,
|
||||
pointer_software_rendering,
|
||||
} = self;
|
||||
|
||||
let x224_processor = x224::Processor::new(
|
||||
static_channels,
|
||||
user_channel_id,
|
||||
io_channel_id,
|
||||
message_channel_id,
|
||||
share_id,
|
||||
);
|
||||
|
||||
// Create bulk decompressor if compression was negotiated
|
||||
let bulk_decompressor = compression_type.and_then(|ct| {
|
||||
let bulk_ct = to_bulk_compression_type(ct);
|
||||
match BulkCompressor::new(bulk_ct) {
|
||||
Ok(compressor) => {
|
||||
info!(compression_type = %bulk_ct, "Bulk decompressor initialized for FastPath");
|
||||
Some(compressor)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Failed to create bulk decompressor, compression disabled");
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let fast_path_processor = fast_path::ProcessorBuilder {
|
||||
io_channel_id,
|
||||
user_channel_id,
|
||||
share_id,
|
||||
enable_server_pointer,
|
||||
pointer_software_rendering,
|
||||
bulk_decompressor,
|
||||
}
|
||||
.build();
|
||||
|
||||
ActiveStage {
|
||||
x224_processor,
|
||||
fast_path_processor,
|
||||
enable_server_pointer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveStage {
|
||||
pub fn update_mouse_pos(&mut self, x: u16, y: u16) {
|
||||
self.fast_path_processor.update_mouse_pos(x, y);
|
||||
}
|
||||
|
||||
/// Encodes outgoing input events and modifies image if necessary (e.g for client-side pointer
|
||||
/// rendering).
|
||||
pub fn process_fastpath_input(
|
||||
&mut self,
|
||||
image: &mut DecodedImage,
|
||||
events: &[FastPathInputEvent],
|
||||
) -> SessionResult<Vec<ActiveStageOutput>> {
|
||||
if events.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Mouse move events are prevalent, so we can preallocate space for
|
||||
// response frame + graphics update
|
||||
let mut output = Vec::with_capacity(2);
|
||||
|
||||
// Encoding fastpath response frame
|
||||
// PERF: unnecessary copy
|
||||
let fastpath_input = FastPathInput::new(events.to_vec()).map_err(SessionError::decode)?;
|
||||
let frame = ironrdp_core::encode_vec(&fastpath_input).map_err(SessionError::encode)?;
|
||||
output.push(ActiveStageOutput::ResponseFrame(frame));
|
||||
|
||||
// If pointer rendering is disabled - we can skip the rest
|
||||
if !self.enable_server_pointer {
|
||||
return Ok(output);
|
||||
}
|
||||
|
||||
// If mouse was moved by client - we should update framebuffer to reflect new
|
||||
// pointer position
|
||||
let mouse_pos = events.iter().find_map(|event| match event {
|
||||
FastPathInputEvent::MouseEvent(event) => Some((event.x_position, event.y_position)),
|
||||
FastPathInputEvent::MouseEventEx(event) => Some((event.x_position, event.y_position)),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let (mouse_x, mouse_y) = match mouse_pos {
|
||||
Some(mouse_pos) => mouse_pos,
|
||||
None => return Ok(output),
|
||||
};
|
||||
|
||||
// Graphics update is only sent when update is visually changed the framebuffer
|
||||
if let Some(rect) = image.move_pointer(mouse_x, mouse_y)? {
|
||||
output.push(ActiveStageOutput::GraphicsUpdate(rect));
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Process a frame received from the server.
|
||||
pub fn process(
|
||||
&mut self,
|
||||
image: &mut DecodedImage,
|
||||
action: Action,
|
||||
frame: &[u8],
|
||||
) -> SessionResult<Vec<ActiveStageOutput>> {
|
||||
let (mut stage_outputs, processor_updates) = match action {
|
||||
Action::FastPath => {
|
||||
let mut output = WriteBuf::new();
|
||||
let processor_updates = self.fast_path_processor.process(image, frame, &mut output)?;
|
||||
(
|
||||
vec![ActiveStageOutput::ResponseFrame(output.into_inner())],
|
||||
processor_updates,
|
||||
)
|
||||
}
|
||||
Action::X224 => {
|
||||
let x224_outputs = self.x224_processor.process(frame)?;
|
||||
let mut stage_outputs = Vec::new();
|
||||
let mut processor_updates = Vec::new();
|
||||
|
||||
for output in x224_outputs {
|
||||
match output {
|
||||
x224::ProcessorOutput::GraphicsUpdate(data) => {
|
||||
let updates = process_slow_path_graphics(&mut self.fast_path_processor, image, &data)?;
|
||||
processor_updates.extend(updates);
|
||||
}
|
||||
x224::ProcessorOutput::PointerUpdate(data) => {
|
||||
let updates = process_slow_path_pointer(&mut self.fast_path_processor, image, &data)?;
|
||||
processor_updates.extend(updates);
|
||||
}
|
||||
other => {
|
||||
stage_outputs.push(ActiveStageOutput::try_from(other)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(stage_outputs, processor_updates)
|
||||
}
|
||||
};
|
||||
|
||||
for update in processor_updates {
|
||||
match update {
|
||||
UpdateKind::None => {}
|
||||
UpdateKind::Region(region) => {
|
||||
stage_outputs.push(ActiveStageOutput::GraphicsUpdate(region));
|
||||
}
|
||||
UpdateKind::PointerDefault => {
|
||||
stage_outputs.push(ActiveStageOutput::PointerDefault);
|
||||
}
|
||||
UpdateKind::PointerHidden => {
|
||||
stage_outputs.push(ActiveStageOutput::PointerHidden);
|
||||
}
|
||||
UpdateKind::PointerPosition { x, y } => {
|
||||
stage_outputs.push(ActiveStageOutput::PointerPosition { x, y });
|
||||
}
|
||||
UpdateKind::PointerBitmap(pointer) => {
|
||||
stage_outputs.push(ActiveStageOutput::PointerBitmap(pointer));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(stage_outputs)
|
||||
}
|
||||
|
||||
pub fn set_fastpath_processor(&mut self, processor: fast_path::Processor) {
|
||||
self.fast_path_processor = processor;
|
||||
}
|
||||
|
||||
/// Updates the share_id used by the x224 processor for encoding ShareDataPdu.
|
||||
/// Must be called during Deactivation-Reactivation if the server assigns a new share_id.
|
||||
pub fn set_share_id(&mut self, share_id: u32) {
|
||||
self.x224_processor.set_share_id(share_id);
|
||||
}
|
||||
|
||||
pub fn set_enable_server_pointer(&mut self, enable_server_pointer: bool) {
|
||||
self.enable_server_pointer = enable_server_pointer;
|
||||
}
|
||||
|
||||
/// Encodes client-side graceful shutdown request. Note that upon sending this request,
|
||||
/// client should wait for server's ShutdownDenied PDU before closing the connection.
|
||||
///
|
||||
/// Client-side graceful shutdown is defined in [MS-RDPBCGR]
|
||||
///
|
||||
/// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68
|
||||
pub fn graceful_shutdown(&self) -> SessionResult<Vec<ActiveStageOutput>> {
|
||||
let mut frame = WriteBuf::new();
|
||||
self.x224_processor
|
||||
.encode_static(&mut frame, ShareDataPdu::ShutdownRequest)?;
|
||||
|
||||
Ok(vec![ActiveStageOutput::ResponseFrame(frame.into_inner())])
|
||||
}
|
||||
|
||||
/// Send a pdu on the static global channel. Typically used to send input events
|
||||
pub fn encode_static(&self, output: &mut WriteBuf, pdu: ShareDataPdu) -> SessionResult<usize> {
|
||||
self.x224_processor.encode_static(output, pdu)
|
||||
}
|
||||
|
||||
pub fn get_svc_processor<T: SvcProcessor + 'static>(&mut self) -> Option<&T> {
|
||||
self.x224_processor.get_svc_processor()
|
||||
}
|
||||
|
||||
pub fn get_svc_processor_mut<T: SvcProcessor + 'static>(&mut self) -> Option<&mut T> {
|
||||
self.x224_processor.get_svc_processor_mut()
|
||||
}
|
||||
|
||||
pub fn get_dvc<T: DvcProcessor + 'static>(&mut self) -> Option<&DynamicVirtualChannel> {
|
||||
self.x224_processor.get_dvc::<T>()
|
||||
}
|
||||
|
||||
pub fn get_dvc_by_channel_id(&mut self, channel_id: u32) -> Option<&DynamicVirtualChannel> {
|
||||
self.x224_processor.get_dvc_by_channel_id(channel_id)
|
||||
}
|
||||
|
||||
/// Completes user's SVC request with data, required to sent it over the network and returns
|
||||
/// a buffer with encoded data.
|
||||
pub fn process_svc_processor_messages<C: SvcProcessor + 'static>(
|
||||
&self,
|
||||
messages: SvcProcessorMessages<C>,
|
||||
) -> SessionResult<Vec<u8>> {
|
||||
self.x224_processor.process_svc_processor_messages(messages)
|
||||
}
|
||||
|
||||
/// Fully encodes a resize request for sending over the Display Control Virtual Channel.
|
||||
///
|
||||
/// If the Display Control Virtual Channel is not available, or not yet connected, this method
|
||||
/// will return `None`.
|
||||
///
|
||||
/// Per [2.2.2.2.1]:
|
||||
/// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value.
|
||||
/// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels.
|
||||
/// - The `scale_factor` MUST be ignored if it is less than 100 percent or greater than 500 percent.
|
||||
/// - The `physical_dims` (width, height) MUST be ignored if either is less than 10 mm or greater than 10,000 mm.
|
||||
///
|
||||
/// Use [`ironrdp_displaycontrol::pdu::MonitorLayoutEntry::adjust_display_size`] to adjust `width` and `height` before calling this function
|
||||
/// to ensure the display size is within the valid range.
|
||||
///
|
||||
/// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c
|
||||
pub fn encode_resize(
|
||||
&mut self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
scale_factor: Option<u32>,
|
||||
physical_dims: Option<(u32, u32)>,
|
||||
) -> Option<SessionResult<Vec<u8>>> {
|
||||
if let Some(dvc) = self.get_dvc::<DisplayControlClient>() {
|
||||
if let Some(channel_id) = dvc.channel_id() {
|
||||
let display_control = dvc.channel_processor_downcast_ref::<DisplayControlClient>()?;
|
||||
let svc_messages = match display_control.encode_single_primary_monitor(
|
||||
channel_id,
|
||||
width,
|
||||
height,
|
||||
scale_factor,
|
||||
physical_dims,
|
||||
) {
|
||||
Ok(messages) => messages,
|
||||
Err(e) => return Some(Err(SessionError::encode(e))),
|
||||
};
|
||||
|
||||
return Some(
|
||||
self.process_svc_processor_messages(SvcProcessorMessages::<DrdynvcClient>::new(svc_messages)),
|
||||
);
|
||||
} else {
|
||||
debug!("Could not encode a resize: Display Control Virtual Channel is not yet connected");
|
||||
}
|
||||
} else {
|
||||
debug!("Could not encode a resize: Display Control Virtual Channel is not available");
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn encode_dvc_messages(&mut self, messages: Vec<SvcMessage>) -> SessionResult<Vec<u8>> {
|
||||
self.process_svc_processor_messages(SvcProcessorMessages::<DrdynvcClient>::new(messages))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ActiveStageOutput {
|
||||
ResponseFrame(Vec<u8>),
|
||||
GraphicsUpdate(InclusiveRectangle),
|
||||
PointerDefault,
|
||||
PointerHidden,
|
||||
PointerPosition {
|
||||
x: u16,
|
||||
y: u16,
|
||||
},
|
||||
PointerBitmap(Arc<DecodedPointer>),
|
||||
Terminate(GracefulDisconnectReason),
|
||||
/// Received a Server Deactivate All PDU. The consumer should execute the [Deactivation-Reactivation Sequence].
|
||||
///
|
||||
/// [Deactivation-Reactivation Sequence]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dfc234ce-481a-4674-9a5d-2a7bafb14432
|
||||
DeactivateAll,
|
||||
/// Server Initiate Multitransport Request. The application should establish a
|
||||
/// sideband UDP transport using the provided request parameters.
|
||||
///
|
||||
/// See [\[MS-RDPBCGR\] 2.2.15.1].
|
||||
///
|
||||
/// [\[MS-RDPBCGR\] 2.2.15.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/de783158-8b01-4818-8fb0-62523a5b3490
|
||||
MultitransportRequest(MultitransportRequestPdu),
|
||||
/// Server-reported network characteristics ([\[MS-RDPBCGR\] 2.2.14.1.5]).
|
||||
///
|
||||
/// Contains an [`AutoDetectRequest::NetworkCharacteristicsResult`] with
|
||||
/// RTT and/or bandwidth measurements computed by the server.
|
||||
///
|
||||
/// See [\[MS-RDPBCGR\] 2.2.14.1.5].
|
||||
///
|
||||
/// [\[MS-RDPBCGR\] 2.2.14.1.5]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/228ffc5c-b60c-4d3e-9781-ac613f822fdf
|
||||
AutoDetect(AutoDetectRequest),
|
||||
}
|
||||
|
||||
impl TryFrom<x224::ProcessorOutput> for ActiveStageOutput {
|
||||
type Error = SessionError;
|
||||
|
||||
fn try_from(value: x224::ProcessorOutput) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
x224::ProcessorOutput::ResponseFrame(frame) => Ok(Self::ResponseFrame(frame)),
|
||||
x224::ProcessorOutput::Disconnect(desc) => {
|
||||
let desc = match desc {
|
||||
x224::DisconnectDescription::McsDisconnect(reason) => match reason {
|
||||
mcs::DisconnectReason::ProviderInitiated => GracefulDisconnectReason::ServerInitiated,
|
||||
mcs::DisconnectReason::UserRequested => GracefulDisconnectReason::UserInitiated,
|
||||
other => GracefulDisconnectReason::Other(other.description().to_owned()),
|
||||
},
|
||||
x224::DisconnectDescription::ErrorInfo(info) => GracefulDisconnectReason::Other(info.description()),
|
||||
};
|
||||
|
||||
Ok(Self::Terminate(desc))
|
||||
}
|
||||
x224::ProcessorOutput::DeactivateAll => Ok(Self::DeactivateAll),
|
||||
x224::ProcessorOutput::MultitransportRequest(pdu) => Ok(Self::MultitransportRequest(pdu)),
|
||||
x224::ProcessorOutput::AutoDetect(request) => Ok(Self::AutoDetect(request)),
|
||||
// GraphicsUpdate and PointerUpdate are consumed in ActiveStage::process()
|
||||
// before reaching this conversion.
|
||||
x224::ProcessorOutput::GraphicsUpdate(_) | x224::ProcessorOutput::PointerUpdate(_) => Err(
|
||||
SessionError::general("slow-path graphics/pointer updates should be handled before this conversion"),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reasons for graceful disconnect. This type provides GUI-friendly descriptions for
|
||||
/// disconnect reasons.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GracefulDisconnectReason {
|
||||
UserInitiated,
|
||||
ServerInitiated,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl GracefulDisconnectReason {
|
||||
pub fn description(&self) -> String {
|
||||
match self {
|
||||
GracefulDisconnectReason::UserInitiated => "user initiated disconnect".to_owned(),
|
||||
GracefulDisconnectReason::ServerInitiated => "server initiated disconnect".to_owned(),
|
||||
GracefulDisconnectReason::Other(description) => description.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for GracefulDisconnectReason {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.write_str(&self.description())
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse and process a slow-path graphics update through the shared bitmap pipeline.
|
||||
fn process_slow_path_graphics(
|
||||
fast_path_processor: &mut fast_path::Processor,
|
||||
image: &mut DecodedImage,
|
||||
data: &[u8],
|
||||
) -> SessionResult<Vec<UpdateKind>> {
|
||||
let mut src = ReadCursor::new(data);
|
||||
let update_type = slow_path::read_graphics_update_type(&mut src).map_err(SessionError::decode)?;
|
||||
|
||||
match update_type {
|
||||
GraphicsUpdateType::Bitmap => {
|
||||
let bitmap = slow_path::decode_slow_path_bitmap(&mut src).map_err(SessionError::decode)?;
|
||||
fast_path_processor.process_bitmap_update(image, bitmap)
|
||||
}
|
||||
GraphicsUpdateType::Orders => {
|
||||
warn!("Slow-path drawing orders not supported (MS-RDPEGDI)");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
GraphicsUpdateType::Palette => {
|
||||
warn!("Slow-path palette update not supported (8bpp)");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
// Synchronize is an artifact from the T.128 multipoint protocol
|
||||
// and carries no data. Safe to ignore.
|
||||
GraphicsUpdateType::Synchronize => {
|
||||
debug!("Ignoring slow-path synchronize update");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse and process a slow-path pointer update through the shared pointer pipeline.
|
||||
fn process_slow_path_pointer(
|
||||
fast_path_processor: &mut fast_path::Processor,
|
||||
image: &mut DecodedImage,
|
||||
data: &[u8],
|
||||
) -> SessionResult<Vec<UpdateKind>> {
|
||||
let mut src = ReadCursor::new(data);
|
||||
let pointer = slow_path::decode_slow_path_pointer(&mut src).map_err(SessionError::decode)?;
|
||||
fast_path_processor.process_pointer_update(image, pointer)
|
||||
}
|
||||
+821
@@ -0,0 +1,821 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ironrdp_bulk::BulkCompressor;
|
||||
use ironrdp_core::{DecodeErrorKind, ReadCursor, WriteBuf, decode_cursor};
|
||||
use ironrdp_graphics::image_processing::PixelFormat;
|
||||
use ironrdp_graphics::pointer::{DecodedPointer, PointerBitmapTarget};
|
||||
use ironrdp_graphics::rdp6::BitmapStreamDecoder;
|
||||
use ironrdp_graphics::rle::RlePixelFormat;
|
||||
use ironrdp_pdu::bitmap::BitmapUpdateData;
|
||||
use ironrdp_pdu::codecs::rfx::FrameAcknowledgePdu;
|
||||
use ironrdp_pdu::fast_path::{FastPathHeader, FastPathUpdate, FastPathUpdatePdu, Fragmentation};
|
||||
use ironrdp_pdu::geometry::{InclusiveRectangle, Rectangle as _};
|
||||
use ironrdp_pdu::pointer::PointerUpdateData;
|
||||
use ironrdp_pdu::rdp::capability_sets::{CODEC_ID_NONE, CODEC_ID_REMOTEFX, CodecId};
|
||||
use ironrdp_pdu::rdp::headers::{CompressionFlags, ShareDataPdu};
|
||||
use ironrdp_pdu::surface_commands::{FrameAction, FrameMarkerPdu, SurfaceCommand};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
use crate::image::DecodedImage;
|
||||
use crate::palette::Palette;
|
||||
use crate::pointer::PointerCache;
|
||||
use crate::{SessionError, SessionErrorExt as _, SessionResult, custom_err, reason_err, rfx};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ironrdp_pdu::bitmap::{BitmapData, Compression};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn raw_bitmap_removes_byte_padding_without_changing_source_stride() {
|
||||
let mut processor = ProcessorBuilder {
|
||||
io_channel_id: 0,
|
||||
user_channel_id: 0,
|
||||
share_id: 0,
|
||||
enable_server_pointer: false,
|
||||
pointer_software_rendering: false,
|
||||
bulk_decompressor: None,
|
||||
}
|
||||
.build();
|
||||
let mut image = DecodedImage::new(PixelFormat::RgbA32, 4, 3);
|
||||
|
||||
// Two bottom-up BGR rows of three source pixels each. The final source
|
||||
// column is outside the destination and each row has three pad bytes.
|
||||
let bitmap_data = [
|
||||
3, 2, 1, 6, 5, 4, 9, 8, 7, 0xff, 0xff, 0xff, // bottom row
|
||||
15, 14, 13, 18, 17, 16, 21, 20, 19, 0xff, 0xff, 0xff, // top row
|
||||
];
|
||||
let bitmap_update = BitmapUpdateData {
|
||||
rectangles: vec![BitmapData {
|
||||
rectangle: InclusiveRectangle {
|
||||
left: 1,
|
||||
top: 0,
|
||||
right: 2,
|
||||
bottom: 1,
|
||||
},
|
||||
width: 3,
|
||||
height: 2,
|
||||
bits_per_pixel: 24,
|
||||
compression_flags: Compression::empty(),
|
||||
compressed_data_header: None,
|
||||
bitmap_data: &bitmap_data,
|
||||
}],
|
||||
};
|
||||
|
||||
processor.process_bitmap_update(&mut image, bitmap_update).unwrap();
|
||||
|
||||
let pixel = |x: usize, y: usize| -> [u8; 4] {
|
||||
let offset = (y * usize::from(image.width()) + x) * 4;
|
||||
image.data()[offset..offset + 4]
|
||||
.try_into()
|
||||
.expect("pixel has four channels")
|
||||
};
|
||||
assert_eq!(pixel(1, 0), [13, 14, 15, 255]);
|
||||
assert_eq!(pixel(2, 0), [16, 17, 18, 255]);
|
||||
assert_eq!(pixel(1, 1), [1, 2, 3, 255]);
|
||||
assert_eq!(pixel(2, 1), [4, 5, 6, 255]);
|
||||
assert_eq!(pixel(3, 0), [0, 0, 0, 0]);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UpdateKind {
|
||||
None,
|
||||
Region(InclusiveRectangle),
|
||||
PointerDefault,
|
||||
PointerHidden,
|
||||
PointerPosition { x: u16, y: u16 },
|
||||
PointerBitmap(Arc<DecodedPointer>),
|
||||
}
|
||||
|
||||
pub struct Processor {
|
||||
complete_data: CompleteData,
|
||||
rfx_handler: rfx::DecodingContext,
|
||||
marker_processor: FrameMarkerProcessor,
|
||||
bitmap_stream_decoder: BitmapStreamDecoder,
|
||||
pointer_cache: PointerCache,
|
||||
use_system_pointer: bool,
|
||||
mouse_pos_update: Option<(u16, u16)>,
|
||||
enable_server_pointer: bool,
|
||||
pointer_software_rendering: bool,
|
||||
/// Bulk decompressor for server-to-client compressed PDUs.
|
||||
/// `None` when compression was not negotiated.
|
||||
bulk_decompressor: Option<BulkCompressor>,
|
||||
/// Current 8bpp color palette. Updated by Palette fast-path updates.
|
||||
palette: Palette,
|
||||
#[cfg(feature = "qoiz")]
|
||||
zdctx: zstd_safe::DCtx<'static>,
|
||||
}
|
||||
|
||||
impl Processor {
|
||||
pub fn update_mouse_pos(&mut self, x: u16, y: u16) {
|
||||
self.mouse_pos_update = Some((x, y));
|
||||
}
|
||||
|
||||
/// Process input fast path frame and return list of updates.
|
||||
pub fn process(
|
||||
&mut self,
|
||||
image: &mut DecodedImage,
|
||||
input: &[u8],
|
||||
output: &mut WriteBuf,
|
||||
) -> SessionResult<Vec<UpdateKind>> {
|
||||
let mut processor_updates = Vec::new();
|
||||
|
||||
if let Some((x, y)) = self.mouse_pos_update.take() {
|
||||
if let Some(rect) = image.move_pointer(x, y)? {
|
||||
processor_updates.push(UpdateKind::Region(rect));
|
||||
}
|
||||
}
|
||||
|
||||
let mut input = ReadCursor::new(input);
|
||||
|
||||
let header = decode_cursor::<FastPathHeader>(&mut input).map_err(SessionError::decode)?;
|
||||
trace!(fast_path_header = ?header, "Received Fast-Path packet");
|
||||
|
||||
// A single FastPath output PDU can contain multiple updates.
|
||||
// Loop over all updates within the PDU payload.
|
||||
while !input.is_empty() {
|
||||
let update_result = self.process_single_update(&mut input, image, output)?;
|
||||
processor_updates.extend(update_result);
|
||||
}
|
||||
|
||||
Ok(processor_updates)
|
||||
}
|
||||
|
||||
/// Process a single FastPath update from the cursor, advancing past it.
|
||||
fn process_single_update(
|
||||
&mut self,
|
||||
input: &mut ReadCursor<'_>,
|
||||
image: &mut DecodedImage,
|
||||
output: &mut WriteBuf,
|
||||
) -> SessionResult<Vec<UpdateKind>> {
|
||||
let mut processor_updates = Vec::new();
|
||||
|
||||
let update_pdu = decode_cursor::<FastPathUpdatePdu<'_>>(input).map_err(SessionError::decode)?;
|
||||
trace!(fast_path_update_fragmentation = ?update_pdu.fragmentation);
|
||||
|
||||
// Decompress the payload if the server sent it compressed.
|
||||
let decompressed_data;
|
||||
let payload = if let Some(flags) = update_pdu.compression_flags {
|
||||
if flags.contains(CompressionFlags::COMPRESSED) || flags.contains(CompressionFlags::FLUSHED) {
|
||||
let bulk_flags =
|
||||
u32::from(flags.bits()) | u32::from(update_pdu.compression_type.map_or(0, |ct| ct.as_u8()));
|
||||
|
||||
if let Some(ref mut decompressor) = self.bulk_decompressor {
|
||||
let decompressed = decompressor
|
||||
.decompress(update_pdu.data, bulk_flags)
|
||||
.map_err(|e| reason_err!("FastPath", "bulk decompression failed: {}", e))?;
|
||||
// Copy decompressed data before accessing metrics (releases the mutable borrow).
|
||||
decompressed_data = decompressed.to_vec();
|
||||
debug!(
|
||||
compressed_size = update_pdu.data.len(),
|
||||
decompressed_size = decompressed_data.len(),
|
||||
compression_type = ?update_pdu.compression_type,
|
||||
compression_ratio = format_args!("{:.2}x", decompressor.compression_ratio()),
|
||||
total_compressed = decompressor.total_compressed_bytes(),
|
||||
total_uncompressed = decompressor.total_uncompressed_bytes(),
|
||||
"Decompressed FastPath update"
|
||||
);
|
||||
decompressed_data.as_slice()
|
||||
} else {
|
||||
warn!("Received compressed FastPath data but no decompressor is configured");
|
||||
update_pdu.data
|
||||
}
|
||||
} else {
|
||||
// Compression flags present but COMPRESSED bit not set — pass data through.
|
||||
// Still need to inform the decompressor of FLUSHED/AT_FRONT flags even
|
||||
// without compressed payload.
|
||||
update_pdu.data
|
||||
}
|
||||
} else {
|
||||
update_pdu.data
|
||||
};
|
||||
|
||||
let processed_complete_data = self.complete_data.process_data(payload, update_pdu.fragmentation);
|
||||
|
||||
let update_code = update_pdu.update_code;
|
||||
|
||||
let Some(data) = processed_complete_data else {
|
||||
return Ok(processor_updates);
|
||||
};
|
||||
|
||||
let update = FastPathUpdate::decode_with_code(data.as_slice(), update_code);
|
||||
|
||||
match update {
|
||||
Ok(FastPathUpdate::SurfaceCommands(surface_commands)) => {
|
||||
trace!("Received Surface Commands: {} pieces", surface_commands.len());
|
||||
let update_region = self.process_surface_commands(image, output, surface_commands)?;
|
||||
processor_updates.push(UpdateKind::Region(update_region));
|
||||
}
|
||||
Ok(FastPathUpdate::Bitmap(bitmap_update)) => {
|
||||
trace!("Received bitmap update");
|
||||
let updates = self.process_bitmap_update(image, bitmap_update)?;
|
||||
processor_updates.extend(updates);
|
||||
}
|
||||
Ok(FastPathUpdate::Pointer(update)) => {
|
||||
let updates = self.process_pointer_update(image, update)?;
|
||||
processor_updates.extend(updates);
|
||||
}
|
||||
Ok(FastPathUpdate::Palette(palette_data)) => {
|
||||
trace!("Received palette update");
|
||||
self.palette.process_update(palette_data);
|
||||
}
|
||||
Err(e) => {
|
||||
// FIXME: This seems to be a way of special-handling the error case in FastPathUpdate::decode_cursor_with_code
|
||||
// to ignore the unsupported update PDUs, but this is a fragile logic and the rationale behind it is not
|
||||
// obvious.
|
||||
if let DecodeErrorKind::InvalidField { field, reason } = e.kind() {
|
||||
warn!(field, reason, "Received invalid Fast-Path update");
|
||||
processor_updates.push(UpdateKind::None);
|
||||
} else {
|
||||
return Err(custom_err!("Fast-Path", e));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(processor_updates)
|
||||
}
|
||||
|
||||
/// Process a bitmap update, shared between fast-path and slow-path pipelines.
|
||||
pub fn process_bitmap_update(
|
||||
&mut self,
|
||||
image: &mut DecodedImage,
|
||||
bitmap_update: BitmapUpdateData<'_>,
|
||||
) -> SessionResult<Vec<UpdateKind>> {
|
||||
let mut buf = Vec::new();
|
||||
let mut update_kind = UpdateKind::None;
|
||||
|
||||
for update in bitmap_update.rectangles {
|
||||
trace!("{update:?}");
|
||||
buf.clear();
|
||||
|
||||
let width = update.width.min(update.rectangle.width());
|
||||
let height = update.height.min(update.rectangle.height());
|
||||
if width == 0 || height == 0 {
|
||||
warn!(
|
||||
bitmap_width = update.width,
|
||||
bitmap_height = update.height,
|
||||
destination = ?update.rectangle,
|
||||
"Skipping bitmap with an empty source or destination"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// `width` and `height` describe the source bitmap layout, while
|
||||
// destination bounds describe where its visible portion is drawn.
|
||||
// Servers can send source rows wider than the destination; preserve
|
||||
// that source stride and discard only the excess source extent.
|
||||
let bitmap_rectangle = InclusiveRectangle {
|
||||
left: update.rectangle.left,
|
||||
top: update.rectangle.top,
|
||||
right: update.rectangle.left + width - 1,
|
||||
bottom: update.rectangle.top + height - 1,
|
||||
};
|
||||
|
||||
// Bitmap data is either compressed or uncompressed, depending
|
||||
// on whether the BITMAP_COMPRESSION flag is present in the
|
||||
// flags field.
|
||||
let update_rectangle = if update
|
||||
.compression_flags
|
||||
.contains(ironrdp_pdu::bitmap::Compression::BITMAP_COMPRESSION)
|
||||
{
|
||||
if update.bits_per_pixel == 32 {
|
||||
// Compressed bitmaps at a color depth of 32 bpp are compressed using RDP 6.0
|
||||
// Bitmap Compression and stored inside an RDP 6.0 Bitmap Compressed Stream
|
||||
// structure ([MS-RDPEGDI] section 2.2.2.5.1).
|
||||
debug!("32 bpp compressed RDP6_BITMAP_STREAM");
|
||||
|
||||
match self.bitmap_stream_decoder.decode_bitmap_stream_to_rgb24(
|
||||
update.bitmap_data,
|
||||
&mut buf,
|
||||
usize::from(update.width),
|
||||
usize::from(update.height),
|
||||
) {
|
||||
// RDP6 bitmap streams are bottom-up (upstream #1524).
|
||||
Ok(()) => image.apply_rgb24(&buf, &bitmap_rectangle, update.width, true)?,
|
||||
Err(err) => {
|
||||
warn!("Invalid RDP6_BITMAP_STREAM: {err}");
|
||||
update.rectangle.clone()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Compressed bitmaps not in 32 bpp format are compressed using Interleaved
|
||||
// RLE and encapsulated in an RLE Compressed Bitmap Stream structure (section
|
||||
// 2.2.9.1.1.3.1.2.4).
|
||||
debug!(bpp = update.bits_per_pixel, "Non-32 bpp compressed RLE_BITMAP_STREAM",);
|
||||
|
||||
match ironrdp_graphics::rle::decompress(
|
||||
update.bitmap_data,
|
||||
&mut buf,
|
||||
usize::from(update.width),
|
||||
usize::from(update.height),
|
||||
usize::from(update.bits_per_pixel),
|
||||
) {
|
||||
Ok(RlePixelFormat::Rgb16) => image.apply_rgb16_bitmap(&buf, &bitmap_rectangle, update.width)?,
|
||||
Ok(RlePixelFormat::Rgb15) => image.apply_rgb15_bitmap(&buf, &bitmap_rectangle, update.width)?,
|
||||
Ok(RlePixelFormat::Rgb24) => image.apply_bgr24_bitmap(&buf, &bitmap_rectangle, update.width)?,
|
||||
Ok(RlePixelFormat::Rgb8) => image.apply_rgb8_with_palette(
|
||||
&buf,
|
||||
&bitmap_rectangle,
|
||||
self.palette.colors(),
|
||||
update.width,
|
||||
)?,
|
||||
|
||||
Err(e) => {
|
||||
warn!("Invalid RLE-compressed bitmap: {e}");
|
||||
update.rectangle.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Uncompressed bitmap data is formatted as a bottom-up, left-to-right series of
|
||||
// pixels. Each pixel is a whole number of bytes. Each row contains a multiple of
|
||||
// four bytes (including up to three bytes of padding, as necessary).
|
||||
// [MS-RDPBCGR] 2.2.9.1.1.3.1.2.2
|
||||
trace!("Uncompressed raw bitmap");
|
||||
|
||||
let bpp = usize::from(update.bits_per_pixel);
|
||||
let width = usize::from(update.width);
|
||||
let bytes_per_pixel = bpp.div_ceil(8);
|
||||
let row_bytes = width * bytes_per_pixel;
|
||||
let padded_row_bytes = (row_bytes + 3) & !3;
|
||||
|
||||
if padded_row_bytes != row_bytes {
|
||||
// Strip only byte padding; the tightly packed rows still retain
|
||||
// `update.width` pixels and therefore their source stride.
|
||||
buf.clear();
|
||||
for row in update
|
||||
.bitmap_data
|
||||
.chunks_exact(padded_row_bytes)
|
||||
.take(usize::from(update.height))
|
||||
{
|
||||
buf.extend_from_slice(&row[..row_bytes]);
|
||||
}
|
||||
|
||||
match update.bits_per_pixel {
|
||||
8 => image.apply_rgb8_with_palette(
|
||||
&buf,
|
||||
&bitmap_rectangle,
|
||||
self.palette.colors(),
|
||||
update.width,
|
||||
)?,
|
||||
15 => image.apply_rgb15_bitmap(&buf, &bitmap_rectangle, update.width)?,
|
||||
16 => image.apply_rgb16_bitmap(&buf, &bitmap_rectangle, update.width)?,
|
||||
24 => image.apply_bgr24_bitmap(&buf, &bitmap_rectangle, update.width)?,
|
||||
32 => image.apply_rgb32_bitmap(&buf, PixelFormat::BgrX32, &bitmap_rectangle, update.width)?,
|
||||
_ => {
|
||||
warn!("Unsupported uncompressed bitmap depth: {bpp} bpp");
|
||||
update.rectangle.clone()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match update.bits_per_pixel {
|
||||
8 => image.apply_rgb8_with_palette(
|
||||
update.bitmap_data,
|
||||
&bitmap_rectangle,
|
||||
self.palette.colors(),
|
||||
update.width,
|
||||
)?,
|
||||
15 => image.apply_rgb15_bitmap(update.bitmap_data, &bitmap_rectangle, update.width)?,
|
||||
16 => image.apply_rgb16_bitmap(update.bitmap_data, &bitmap_rectangle, update.width)?,
|
||||
24 => image.apply_bgr24_bitmap(update.bitmap_data, &bitmap_rectangle, update.width)?,
|
||||
32 => image.apply_rgb32_bitmap(
|
||||
update.bitmap_data,
|
||||
PixelFormat::BgrX32,
|
||||
&bitmap_rectangle,
|
||||
update.width,
|
||||
)?,
|
||||
_ => {
|
||||
warn!("Unsupported uncompressed bitmap depth: {bpp} bpp");
|
||||
update.rectangle.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match update_kind {
|
||||
UpdateKind::Region(current) => update_kind = UpdateKind::Region(current.union(&update_rectangle)),
|
||||
_ => update_kind = UpdateKind::Region(update_rectangle),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(vec![update_kind])
|
||||
}
|
||||
|
||||
/// Process a pointer update, shared between fast-path and slow-path pipelines.
|
||||
pub fn process_pointer_update(
|
||||
&mut self,
|
||||
image: &mut DecodedImage,
|
||||
update: PointerUpdateData<'_>,
|
||||
) -> SessionResult<Vec<UpdateKind>> {
|
||||
let mut processor_updates = Vec::new();
|
||||
|
||||
if !self.enable_server_pointer {
|
||||
return Ok(processor_updates);
|
||||
}
|
||||
|
||||
let bitmap_target = if self.pointer_software_rendering {
|
||||
PointerBitmapTarget::Software
|
||||
} else {
|
||||
PointerBitmapTarget::Accelerated
|
||||
};
|
||||
|
||||
match update {
|
||||
PointerUpdateData::SetHidden => {
|
||||
processor_updates.push(UpdateKind::PointerHidden);
|
||||
if self.pointer_software_rendering && !self.use_system_pointer {
|
||||
self.use_system_pointer = true;
|
||||
if let Some(rect) = image.hide_pointer()? {
|
||||
processor_updates.push(UpdateKind::Region(rect));
|
||||
}
|
||||
}
|
||||
}
|
||||
PointerUpdateData::SetDefault => {
|
||||
processor_updates.push(UpdateKind::PointerDefault);
|
||||
if self.pointer_software_rendering && !self.use_system_pointer {
|
||||
self.use_system_pointer = true;
|
||||
if let Some(rect) = image.hide_pointer()? {
|
||||
processor_updates.push(UpdateKind::Region(rect));
|
||||
}
|
||||
}
|
||||
}
|
||||
PointerUpdateData::SetPosition(position) => {
|
||||
if self.use_system_pointer || !self.pointer_software_rendering {
|
||||
processor_updates.push(UpdateKind::PointerPosition {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
});
|
||||
} else if let Some(rect) = image.move_pointer(position.x, position.y)? {
|
||||
processor_updates.push(UpdateKind::Region(rect));
|
||||
}
|
||||
}
|
||||
PointerUpdateData::Color(pointer) => {
|
||||
let cache_index = pointer.cache_index;
|
||||
|
||||
let decoded_pointer = Arc::new(
|
||||
DecodedPointer::decode_color_pointer_attribute(&pointer, bitmap_target)
|
||||
.map_err(|e| SessionError::custom("failed to decode color pointer attribute", e))?,
|
||||
);
|
||||
|
||||
let _ = self
|
||||
.pointer_cache
|
||||
.insert(usize::from(cache_index), Arc::clone(&decoded_pointer));
|
||||
|
||||
if !self.pointer_software_rendering {
|
||||
processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&decoded_pointer)));
|
||||
} else if let Some(rect) = image.update_pointer(decoded_pointer)? {
|
||||
processor_updates.push(UpdateKind::Region(rect));
|
||||
}
|
||||
}
|
||||
PointerUpdateData::Cached(cached) => {
|
||||
let cache_index = cached.cache_index;
|
||||
|
||||
if let Some(cached_pointer) = self.pointer_cache.get(usize::from(cache_index)) {
|
||||
// Disable system pointer
|
||||
processor_updates.push(UpdateKind::PointerHidden);
|
||||
self.use_system_pointer = false;
|
||||
// Send graphics update
|
||||
if !self.pointer_software_rendering {
|
||||
processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&cached_pointer)));
|
||||
} else if let Some(rect) = image.update_pointer(cached_pointer)? {
|
||||
processor_updates.push(UpdateKind::Region(rect));
|
||||
} else {
|
||||
// In case pointer was hidden previously
|
||||
if let Some(rect) = image.show_pointer()? {
|
||||
processor_updates.push(UpdateKind::Region(rect));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("Cached pointer not found {}", cache_index);
|
||||
}
|
||||
}
|
||||
PointerUpdateData::New(pointer) => {
|
||||
let cache_index = pointer.color_pointer.cache_index;
|
||||
|
||||
let decoded_pointer = Arc::new(
|
||||
DecodedPointer::decode_pointer_attribute(&pointer, bitmap_target)
|
||||
.map_err(|e| SessionError::custom("failed to decode pointer attribute", e))?,
|
||||
);
|
||||
|
||||
let _ = self
|
||||
.pointer_cache
|
||||
.insert(usize::from(cache_index), Arc::clone(&decoded_pointer));
|
||||
|
||||
if !self.pointer_software_rendering {
|
||||
processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&decoded_pointer)));
|
||||
} else if let Some(rect) = image.update_pointer(decoded_pointer)? {
|
||||
processor_updates.push(UpdateKind::Region(rect));
|
||||
}
|
||||
}
|
||||
PointerUpdateData::Large(pointer) => {
|
||||
let cache_index = pointer.cache_index;
|
||||
|
||||
let decoded_pointer: Arc<DecodedPointer> = Arc::new(
|
||||
DecodedPointer::decode_large_pointer_attribute(&pointer, bitmap_target)
|
||||
.map_err(|e| SessionError::custom("failed to decode large pointer attribute", e))?,
|
||||
);
|
||||
|
||||
let _ = self
|
||||
.pointer_cache
|
||||
.insert(usize::from(cache_index), Arc::clone(&decoded_pointer));
|
||||
|
||||
if !self.pointer_software_rendering {
|
||||
processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&decoded_pointer)));
|
||||
} else if let Some(rect) = image.update_pointer(decoded_pointer)? {
|
||||
processor_updates.push(UpdateKind::Region(rect));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(processor_updates)
|
||||
}
|
||||
|
||||
fn process_surface_commands(
|
||||
&mut self,
|
||||
image: &mut DecodedImage,
|
||||
output: &mut WriteBuf,
|
||||
surface_commands: Vec<SurfaceCommand<'_>>,
|
||||
) -> SessionResult<InclusiveRectangle> {
|
||||
let mut update_rectangle = None;
|
||||
|
||||
for command in surface_commands {
|
||||
match command {
|
||||
SurfaceCommand::SetSurfaceBits(bits) | SurfaceCommand::StreamSurfaceBits(bits) => {
|
||||
let codec_id = CodecId::from_u8(bits.extended_bitmap_data.codec_id).ok_or_else(|| {
|
||||
reason_err!(
|
||||
"Fast-Path",
|
||||
"unexpected codec ID: {:x}",
|
||||
bits.extended_bitmap_data.codec_id
|
||||
)
|
||||
})?;
|
||||
|
||||
trace!(?codec_id, "Surface bits");
|
||||
|
||||
let destination = bits.destination;
|
||||
// TODO(@pacmancoder): Correct rectangle conversion logic should
|
||||
// be revisited when `rectangle_processing.rs` from
|
||||
// `ironrdp-graphics` will be refactored to use generic `Rectangle`
|
||||
// trait instead of hardcoded `InclusiveRectangle`.
|
||||
let destination = InclusiveRectangle {
|
||||
left: destination.left,
|
||||
top: destination.top,
|
||||
right: destination.right - 1,
|
||||
bottom: destination.bottom - 1,
|
||||
};
|
||||
match codec_id {
|
||||
CODEC_ID_NONE => {
|
||||
let ext_data = bits.extended_bitmap_data;
|
||||
let source_width = destination.width();
|
||||
let rectangle = match ext_data.bpp {
|
||||
8 => image.apply_rgb8_with_palette(
|
||||
ext_data.data,
|
||||
&destination,
|
||||
self.palette.colors(),
|
||||
source_width,
|
||||
)?,
|
||||
15 => image.apply_rgb15_bitmap(ext_data.data, &destination, source_width)?,
|
||||
16 => image.apply_rgb16_bitmap(ext_data.data, &destination, source_width)?,
|
||||
24 => image.apply_bgr24_bitmap(ext_data.data, &destination, source_width)?,
|
||||
32 => image.apply_rgb32_bitmap(
|
||||
ext_data.data,
|
||||
PixelFormat::BgrX32,
|
||||
&destination,
|
||||
source_width,
|
||||
)?,
|
||||
bpp => {
|
||||
warn!("Unsupported surface CODEC_ID_NONE bpp: {bpp}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
update_rectangle = update_rectangle
|
||||
.map(|rect: InclusiveRectangle| rect.union(&rectangle))
|
||||
.or(Some(rectangle));
|
||||
}
|
||||
CODEC_ID_REMOTEFX => {
|
||||
let mut data = ReadCursor::new(bits.extended_bitmap_data.data);
|
||||
while !data.is_empty() {
|
||||
let (_frame_id, rectangle) = self.rfx_handler.decode(image, &destination, &mut data)?;
|
||||
update_rectangle = update_rectangle
|
||||
.map(|rect: InclusiveRectangle| rect.union(&rectangle))
|
||||
.or(Some(rectangle));
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "qoi")]
|
||||
ironrdp_pdu::rdp::capability_sets::CODEC_ID_QOI => {
|
||||
qoi_apply(
|
||||
image,
|
||||
destination,
|
||||
bits.extended_bitmap_data.data,
|
||||
&mut update_rectangle,
|
||||
)?;
|
||||
}
|
||||
#[cfg(feature = "qoiz")]
|
||||
ironrdp_pdu::rdp::capability_sets::CODEC_ID_QOIZ => {
|
||||
let compressed = &bits.extended_bitmap_data.data;
|
||||
let mut input = zstd_safe::InBuffer::around(compressed);
|
||||
let mut data = vec![0; compressed.len() * 4];
|
||||
let mut pos = 0;
|
||||
loop {
|
||||
let mut output = zstd_safe::OutBuffer::around_pos(data.as_mut_slice(), pos);
|
||||
self.zdctx
|
||||
.decompress_stream(&mut output, &mut input)
|
||||
.map_err(zstd_safe::get_error_name)
|
||||
.map_err(|e| reason_err!("zstd", "{}", e))?;
|
||||
pos = output.pos();
|
||||
if pos == output.capacity() {
|
||||
data.resize(data.capacity() * 2, 0);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
qoi_apply(image, destination, &data, &mut update_rectangle)?;
|
||||
}
|
||||
_ => {
|
||||
warn!("Unsupported codec ID: {}", bits.extended_bitmap_data.codec_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
SurfaceCommand::FrameMarker(marker) => {
|
||||
trace!(
|
||||
"Frame marker: action {:?} with ID #{}",
|
||||
marker.frame_action,
|
||||
marker.frame_id.unwrap_or(0)
|
||||
);
|
||||
self.marker_processor.process(&marker, output)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(update_rectangle.unwrap_or_else(InclusiveRectangle::empty))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "qoi")]
|
||||
fn qoi_apply(
|
||||
image: &mut DecodedImage,
|
||||
destination: InclusiveRectangle,
|
||||
data: &[u8],
|
||||
update_rectangle: &mut Option<InclusiveRectangle>,
|
||||
) -> SessionResult<()> {
|
||||
let (header, decoded) = qoi::decode_to_vec(data).map_err(|e| reason_err!("QOI decode", "{}", e))?;
|
||||
|
||||
// Guard against a decoded buffer that doesn't match the destination
|
||||
// rectangle. `apply_rgb24`/`apply_rgba32` derive the row count from the
|
||||
// decoded length, and the only bounds check downstream (`rect_fits`)
|
||||
// validates the rectangle against the image, not the buffer against the
|
||||
// rectangle. A malformed/oversized QOI payload would otherwise drive the
|
||||
// per-row index past `self.data` and panic (client-side DoS).
|
||||
let channels = match header.channels {
|
||||
qoi::Channels::Rgb => 3,
|
||||
qoi::Channels::Rgba => 4,
|
||||
};
|
||||
let expected = usize::from(destination.width()) * usize::from(destination.height()) * channels;
|
||||
if decoded.len() != expected {
|
||||
return Err(reason_err!(
|
||||
"QOI decode",
|
||||
"decoded {} bytes, expected {} for {}x{} ({} channels)",
|
||||
decoded.len(),
|
||||
expected,
|
||||
destination.width(),
|
||||
destination.height(),
|
||||
channels
|
||||
));
|
||||
}
|
||||
|
||||
let rectangle = match header.channels {
|
||||
qoi::Channels::Rgb => image.apply_rgb24(&decoded, &destination, destination.width(), false)?,
|
||||
qoi::Channels::Rgba => image.apply_rgba32(&decoded, &destination, false)?,
|
||||
};
|
||||
|
||||
*update_rectangle = update_rectangle
|
||||
.as_ref()
|
||||
.map(|rect: &InclusiveRectangle| rect.union(&rectangle))
|
||||
.or(Some(rectangle));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct ProcessorBuilder {
|
||||
pub io_channel_id: u16,
|
||||
pub user_channel_id: u16,
|
||||
pub share_id: u32,
|
||||
/// Ignore server pointer updates.
|
||||
pub enable_server_pointer: bool,
|
||||
/// Use software rendering mode for pointer bitmap generation. When this option is active,
|
||||
/// `UpdateKind::PointerBitmap` will not be generated. Remote pointer will be drawn
|
||||
/// via software rendering on top of the output image.
|
||||
pub pointer_software_rendering: bool,
|
||||
/// Bulk decompressor for server-to-client compressed PDUs.
|
||||
/// `None` when compression was not negotiated.
|
||||
pub bulk_decompressor: Option<BulkCompressor>,
|
||||
}
|
||||
|
||||
impl ProcessorBuilder {
|
||||
pub fn build(self) -> Processor {
|
||||
Processor {
|
||||
complete_data: CompleteData::new(),
|
||||
rfx_handler: rfx::DecodingContext::new(),
|
||||
marker_processor: FrameMarkerProcessor::new(self.user_channel_id, self.io_channel_id, self.share_id),
|
||||
bitmap_stream_decoder: BitmapStreamDecoder::default(),
|
||||
pointer_cache: PointerCache::default(),
|
||||
use_system_pointer: true,
|
||||
mouse_pos_update: None,
|
||||
enable_server_pointer: self.enable_server_pointer,
|
||||
pointer_software_rendering: self.pointer_software_rendering,
|
||||
bulk_decompressor: self.bulk_decompressor,
|
||||
palette: Palette::system_default(),
|
||||
#[cfg(feature = "qoiz")]
|
||||
zdctx: zstd_safe::DCtx::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct CompleteData {
|
||||
fragmented_data: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl CompleteData {
|
||||
fn new() -> Self {
|
||||
Self { fragmented_data: None }
|
||||
}
|
||||
|
||||
fn process_data(&mut self, data: &[u8], fragmentation: Fragmentation) -> Option<Vec<u8>> {
|
||||
match fragmentation {
|
||||
Fragmentation::Single => {
|
||||
self.check_data_is_empty();
|
||||
|
||||
Some(data.to_vec())
|
||||
}
|
||||
Fragmentation::First => {
|
||||
self.check_data_is_empty();
|
||||
|
||||
self.fragmented_data = Some(data.to_vec());
|
||||
|
||||
None
|
||||
}
|
||||
Fragmentation::Next => {
|
||||
self.append_data(data);
|
||||
|
||||
None
|
||||
}
|
||||
Fragmentation::Last => {
|
||||
self.append_data(data);
|
||||
|
||||
self.fragmented_data.take()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_data_is_empty(&mut self) {
|
||||
if self.fragmented_data.is_some() {
|
||||
warn!("Skipping pending Fast-Path Update internal multiple elements data");
|
||||
self.fragmented_data = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn append_data(&mut self, data: &[u8]) {
|
||||
if let Some(fragmented_data) = self.fragmented_data.as_mut() {
|
||||
fragmented_data.extend_from_slice(data);
|
||||
} else {
|
||||
warn!("Got unexpected Next fragmentation PDU without prior First fragmentation PDU");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FrameMarkerProcessor {
|
||||
user_channel_id: u16,
|
||||
io_channel_id: u16,
|
||||
share_id: u32,
|
||||
}
|
||||
|
||||
impl FrameMarkerProcessor {
|
||||
fn new(user_channel_id: u16, io_channel_id: u16, share_id: u32) -> Self {
|
||||
Self {
|
||||
user_channel_id,
|
||||
io_channel_id,
|
||||
share_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn process(&mut self, marker: &FrameMarkerPdu, output: &mut WriteBuf) -> SessionResult<()> {
|
||||
match marker.frame_action {
|
||||
FrameAction::Begin => Ok(()),
|
||||
FrameAction::End => {
|
||||
ironrdp_pdu::rdp::headers::encode_share_data(
|
||||
self.user_channel_id,
|
||||
self.io_channel_id,
|
||||
self.share_id,
|
||||
ShareDataPdu::FrameAcknowledge(FrameAcknowledgePdu {
|
||||
frame_id: marker.frame_id.unwrap_or(0),
|
||||
}),
|
||||
output,
|
||||
)
|
||||
.map_err(SessionError::encode)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1038
File diff suppressed because it is too large
Load Diff
+130
@@ -0,0 +1,130 @@
|
||||
#![cfg_attr(doc, doc = include_str!("../README.md"))]
|
||||
#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
|
||||
#![allow(clippy::arithmetic_side_effects)] // FIXME: remove
|
||||
|
||||
mod macros;
|
||||
|
||||
pub mod fast_path;
|
||||
pub mod image;
|
||||
pub mod pointer;
|
||||
pub mod rfx; // FIXME: maybe this module should not be in this crate
|
||||
pub mod x224;
|
||||
|
||||
mod active_stage;
|
||||
mod palette;
|
||||
|
||||
use core::fmt;
|
||||
|
||||
pub use active_stage::{ActiveStage, ActiveStageBuilder, ActiveStageOutput, GracefulDisconnectReason};
|
||||
|
||||
pub type SessionResult<T> = Result<T, SessionError>;
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
pub enum SessionErrorKind {
|
||||
Pdu(ironrdp_pdu::PduError),
|
||||
Encode(ironrdp_core::EncodeError),
|
||||
Decode(ironrdp_core::DecodeError),
|
||||
Reason(String),
|
||||
General,
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl fmt::Display for SessionErrorKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self {
|
||||
SessionErrorKind::Pdu(_) => write!(f, "PDU error"),
|
||||
SessionErrorKind::Encode(_) => write!(f, "encode error"),
|
||||
SessionErrorKind::Decode(_) => write!(f, "decode error"),
|
||||
SessionErrorKind::Reason(description) => write!(f, "reason: {description}"),
|
||||
SessionErrorKind::General => write!(f, "general error"),
|
||||
SessionErrorKind::Custom => write!(f, "custom error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::error::Error for SessionErrorKind {
|
||||
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
|
||||
match &self {
|
||||
SessionErrorKind::Pdu(e) => Some(e),
|
||||
SessionErrorKind::Encode(e) => Some(e),
|
||||
SessionErrorKind::Decode(e) => Some(e),
|
||||
SessionErrorKind::Reason(_) => None,
|
||||
SessionErrorKind::General => None,
|
||||
SessionErrorKind::Custom => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type SessionError = ironrdp_error::Error<SessionErrorKind>;
|
||||
|
||||
pub trait SessionErrorExt {
|
||||
fn pdu(error: ironrdp_pdu::PduError) -> Self;
|
||||
fn encode(error: ironrdp_core::EncodeError) -> Self;
|
||||
fn decode(error: ironrdp_core::DecodeError) -> Self;
|
||||
fn general(context: &'static str) -> Self;
|
||||
fn reason(context: &'static str, reason: impl Into<String>) -> Self;
|
||||
fn custom<E>(context: &'static str, e: E) -> Self
|
||||
where
|
||||
E: core::error::Error + Sync + Send + 'static;
|
||||
}
|
||||
|
||||
impl SessionErrorExt for SessionError {
|
||||
#[track_caller]
|
||||
fn pdu(error: ironrdp_pdu::PduError) -> Self {
|
||||
Self::new("payload error", SessionErrorKind::Pdu(error))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn encode(error: ironrdp_core::EncodeError) -> Self {
|
||||
Self::new("encode error", SessionErrorKind::Encode(error))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn decode(error: ironrdp_core::DecodeError) -> Self {
|
||||
Self::new("decode error", SessionErrorKind::Decode(error))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn general(context: &'static str) -> Self {
|
||||
Self::new(context, SessionErrorKind::General)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn reason(context: &'static str, reason: impl Into<String>) -> Self {
|
||||
Self::new(context, SessionErrorKind::Reason(reason.into()))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn custom<E>(context: &'static str, e: E) -> Self
|
||||
where
|
||||
E: core::error::Error + Sync + Send + 'static,
|
||||
{
|
||||
Self::new(context, SessionErrorKind::Custom).with_source(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SessionResultExt {
|
||||
#[must_use]
|
||||
fn with_context(self, context: &'static str) -> Self;
|
||||
#[must_use]
|
||||
fn with_source<E>(self, source: E) -> Self
|
||||
where
|
||||
E: core::error::Error + Sync + Send + 'static;
|
||||
}
|
||||
|
||||
impl<T> SessionResultExt for SessionResult<T> {
|
||||
fn with_context(self, context: &'static str) -> Self {
|
||||
self.map_err(|mut e| {
|
||||
e.set_context(context);
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
fn with_source<E>(self, source: E) -> Self
|
||||
where
|
||||
E: core::error::Error + Sync + Send + 'static,
|
||||
{
|
||||
self.map_err(|e| e.with_source(source))
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/// Creates a `SessionError` with `General` kind
|
||||
///
|
||||
/// Shorthand for
|
||||
/// ```ignore
|
||||
/// <ironrdp_session::SessionError as ironrdp_session::SessionErrorExt>::general(context)
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! general_err {
|
||||
( $context:expr $(,)? ) => {{ <$crate::SessionError as $crate::SessionErrorExt>::general($context) }};
|
||||
}
|
||||
|
||||
/// Creates a `SessionError` with `Reason` kind
|
||||
///
|
||||
/// Shorthand for
|
||||
/// ```ignore
|
||||
/// <ironrdp_session::SessionError as ironrdp_session::SessionErrorExt>::reason(context, reason)
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! reason_err {
|
||||
( $context:expr, $($arg:tt)* ) => {{
|
||||
<$crate::SessionError as $crate::SessionErrorExt>::reason($context, format!($($arg)*))
|
||||
}};
|
||||
}
|
||||
|
||||
/// Creates a `SessionError` with `Custom` kind and a source error attached to it
|
||||
///
|
||||
/// Shorthand for
|
||||
/// ```ignore
|
||||
/// <ironrdp_session::SessionError as ironrdp_session::SessionErrorExt>::custom(context, source)
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! custom_err {
|
||||
( $context:expr, $source:expr $(,)? ) => {{ <$crate::SessionError as $crate::SessionErrorExt>::custom($context, $source) }};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! eof_try {
|
||||
($e:expr) => {
|
||||
match $e {
|
||||
Err(ref e) if e.kind() == io::ErrorKind::UnexpectedEof => {
|
||||
return Ok(None);
|
||||
}
|
||||
result => result,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! try_ready {
|
||||
($e:expr) => {
|
||||
match $e {
|
||||
Ok(Some(v)) => Ok(v),
|
||||
Ok(None) => return Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
};
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
use tracing::{debug, warn};
|
||||
|
||||
/// 8bpp color palette (256 RGB entries).
|
||||
///
|
||||
/// Initialized with the default Windows system palette (VGA colors)
|
||||
/// per MS-RDPBCGR 2.2.9.1.1.3.1.1. Updated by TS_UPDATE_PALETTE_DATA
|
||||
/// fast-path updates during the session.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Palette {
|
||||
colors: [[u8; 3]; 256],
|
||||
}
|
||||
|
||||
impl Palette {
|
||||
/// Create a palette initialized with the 20 static colors from the
|
||||
/// Windows default system palette. Indices 0-9 and 246-255 are the
|
||||
/// reserved static colors; the middle 236 entries (10-245) are black.
|
||||
///
|
||||
/// Reference: <https://learn.microsoft.com/en-us/windows/win32/gdi/default-palette>
|
||||
pub(crate) fn system_default() -> Self {
|
||||
let mut colors = [[0u8; 3]; 256];
|
||||
// Lower 10 static colors (indices 0-9)
|
||||
colors[0] = [0, 0, 0]; // Black
|
||||
colors[1] = [128, 0, 0]; // Dark Red
|
||||
colors[2] = [0, 128, 0]; // Dark Green
|
||||
colors[3] = [128, 128, 0]; // Dark Yellow
|
||||
colors[4] = [0, 0, 128]; // Dark Blue
|
||||
colors[5] = [128, 0, 128]; // Dark Magenta
|
||||
colors[6] = [0, 128, 128]; // Dark Cyan
|
||||
colors[7] = [192, 192, 192]; // Light Gray
|
||||
colors[8] = [192, 220, 192]; // Money Green
|
||||
colors[9] = [166, 202, 240]; // Sky Blue
|
||||
// Upper 10 static colors (indices 246-255)
|
||||
colors[246] = [255, 251, 240]; // Cream
|
||||
colors[247] = [160, 160, 164]; // Medium Gray
|
||||
colors[248] = [128, 128, 128]; // Dark Gray
|
||||
colors[249] = [255, 0, 0]; // Red
|
||||
colors[250] = [0, 255, 0]; // Green
|
||||
colors[251] = [255, 255, 0]; // Yellow
|
||||
colors[252] = [0, 0, 255]; // Blue
|
||||
colors[253] = [255, 0, 255]; // Magenta
|
||||
colors[254] = [0, 255, 255]; // Cyan
|
||||
colors[255] = [255, 255, 255]; // White
|
||||
Self { colors }
|
||||
}
|
||||
|
||||
/// Parse TS_UPDATE_PALETTE_DATA and update palette entries.
|
||||
/// Wire format: pad(2) + numberColors(u32) + N x TS_COLOR_QUAD [B, G, R, pad].
|
||||
pub(crate) fn process_update(&mut self, data: &[u8]) {
|
||||
if data.len() < 6 {
|
||||
warn!("Palette update too short: {} bytes", data.len());
|
||||
return;
|
||||
}
|
||||
|
||||
let raw_count = u32::from_le_bytes([data[2], data[3], data[4], data[5]]);
|
||||
// Palette can have at most 256 entries; clamp before any arithmetic
|
||||
// to prevent overflow on untrusted input
|
||||
let clamped = raw_count.min(256);
|
||||
let number_colors = usize::try_from(clamped).unwrap_or(256);
|
||||
let entry_data = &data[6..];
|
||||
|
||||
let Some(required_len) = number_colors.checked_mul(4) else {
|
||||
warn!("Palette entry count overflow");
|
||||
return;
|
||||
};
|
||||
|
||||
if entry_data.len() < required_len {
|
||||
warn!(
|
||||
"Palette data truncated: expected {} bytes for {} colors, got {}",
|
||||
required_len,
|
||||
number_colors,
|
||||
entry_data.len()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for i in 0..number_colors {
|
||||
let offset = i * 4;
|
||||
// TS_COLOR_QUAD: Blue, Green, Red, Pad
|
||||
self.colors[i] = [entry_data[offset + 2], entry_data[offset + 1], entry_data[offset]];
|
||||
}
|
||||
|
||||
debug!("Updated palette with {} colors", number_colors);
|
||||
}
|
||||
|
||||
/// Borrow the underlying color table for bitmap application.
|
||||
pub(crate) fn colors(&self) -> &[[u8; 3]; 256] {
|
||||
&self.colors
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ironrdp_graphics::pointer::DecodedPointer;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PointerCache {
|
||||
// TODO(@pacancoder) maybe use Vec<Optional<...>> instead?
|
||||
cache: HashMap<usize, Arc<DecodedPointer>>,
|
||||
}
|
||||
|
||||
impl PointerCache {
|
||||
pub fn insert(&mut self, id: usize, pointer: Arc<DecodedPointer>) -> Option<Arc<DecodedPointer>> {
|
||||
self.cache.insert(id, pointer)
|
||||
}
|
||||
|
||||
pub fn get(&self, id: usize) -> Option<Arc<DecodedPointer>> {
|
||||
self.cache.get(&id).cloned()
|
||||
}
|
||||
|
||||
pub fn is_cached(&self, id: usize) -> bool {
|
||||
self.cache.contains_key(&id)
|
||||
}
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
use core::cmp::min;
|
||||
|
||||
use ironrdp_graphics::color_conversion::{self, YCbCrBuffer};
|
||||
use ironrdp_graphics::image_processing::PixelFormat;
|
||||
use ironrdp_graphics::rectangle_processing::Region;
|
||||
use ironrdp_graphics::{dwt, quantization, rlgr, subband_reconstruction};
|
||||
use ironrdp_pdu::codecs::rfx::{self, EntropyAlgorithm, Quant, RfxRectangle, Tile};
|
||||
use ironrdp_pdu::geometry::{InclusiveRectangle, Rectangle as _};
|
||||
use ironrdp_pdu::{Decode as _, ReadCursor, decode_cursor};
|
||||
use tracing::{instrument, trace};
|
||||
|
||||
use crate::image::DecodedImage;
|
||||
use crate::{SessionResult, custom_err, general_err, reason_err};
|
||||
|
||||
const TILE_SIZE: u16 = 64;
|
||||
|
||||
pub type FrameId = u32;
|
||||
|
||||
pub struct DecodingContext {
|
||||
context: rfx::ContextPdu,
|
||||
channels: rfx::ChannelsPdu,
|
||||
decoding_tiles: DecodingTileContext,
|
||||
}
|
||||
|
||||
impl Default for DecodingContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
context: rfx::ContextPdu {
|
||||
flags: rfx::OperatingMode::empty(),
|
||||
entropy_algorithm: EntropyAlgorithm::Rlgr1,
|
||||
},
|
||||
channels: rfx::ChannelsPdu(Vec::new()),
|
||||
decoding_tiles: DecodingTileContext::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DecodingContext {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn decode(
|
||||
&mut self,
|
||||
image: &mut DecodedImage,
|
||||
destination: &InclusiveRectangle,
|
||||
input: &mut ReadCursor<'_>,
|
||||
) -> SessionResult<(FrameId, InclusiveRectangle)> {
|
||||
loop {
|
||||
let block = rfx::Block::decode(input).map_err(|e| custom_err!("decode block", e))?;
|
||||
|
||||
match block {
|
||||
rfx::Block::Sync(_) => {
|
||||
self.process_sync(input)?;
|
||||
}
|
||||
rfx::Block::CodecChannel(rfx::CodecChannel::FrameBegin(f)) => {
|
||||
return self.process_frame(f, input, image, destination);
|
||||
}
|
||||
_ => {
|
||||
return Err(reason_err!(
|
||||
"rfx::DecodingContext",
|
||||
"unexpected RFX block type: {:?}",
|
||||
block.block_type()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_sync(&mut self, input: &mut ReadCursor<'_>) -> SessionResult<()> {
|
||||
self.process_headers(input)
|
||||
}
|
||||
|
||||
fn process_headers(&mut self, input: &mut ReadCursor<'_>) -> SessionResult<()> {
|
||||
let mut context = None;
|
||||
let mut channels = None;
|
||||
|
||||
// headers can appear in any order: CodecVersions, Channels, Context
|
||||
for _ in 0..3 {
|
||||
match decode_cursor(input).map_err(|e| custom_err!("decode headers", e))? {
|
||||
rfx::Block::CodecChannel(rfx::CodecChannel::Context(c)) => context = Some(c),
|
||||
rfx::Block::Channels(c) => channels = Some(c),
|
||||
rfx::Block::CodecVersions(_) => (),
|
||||
_ => {
|
||||
return Err(general_err!("unexpected RFX block type"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let context = context.ok_or_else(|| general_err!("context header is missing"))?;
|
||||
let channels = channels.ok_or_else(|| general_err!("channels header is missing"))?;
|
||||
|
||||
if channels.0.is_empty() {
|
||||
return Err(general_err!("no RFX channel announced"));
|
||||
}
|
||||
|
||||
self.context = context;
|
||||
self.channels = channels;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
fn process_frame(
|
||||
&mut self,
|
||||
frame_begin: rfx::FrameBeginPdu,
|
||||
input: &mut ReadCursor<'_>,
|
||||
image: &mut DecodedImage,
|
||||
destination: &InclusiveRectangle,
|
||||
) -> SessionResult<(FrameId, InclusiveRectangle)> {
|
||||
let channel = self
|
||||
.channels
|
||||
.0
|
||||
.first()
|
||||
.ok_or_else(|| general_err!("no RFX channel found"))?;
|
||||
let width = channel.width.try_into().map_err(|_| general_err!("invalid width"))?;
|
||||
let height = channel.height.try_into().map_err(|_| general_err!("invalid height"))?;
|
||||
let entropy_algorithm = self.context.entropy_algorithm;
|
||||
|
||||
let region: rfx::Block<'_> = decode_cursor(input).map_err(|e| custom_err!("decode region", e))?;
|
||||
let mut region = match region {
|
||||
rfx::Block::CodecChannel(rfx::CodecChannel::Region(region)) => region,
|
||||
_ => return Err(general_err!("unexpected block type")),
|
||||
};
|
||||
let tile_set: rfx::Block<'_> = decode_cursor(input).map_err(|e| custom_err!("decode tile_set", e))?;
|
||||
let tile_set = match tile_set {
|
||||
rfx::Block::CodecChannel(rfx::CodecChannel::TileSet(t)) => t,
|
||||
_ => return Err(general_err!("unexpected block type")),
|
||||
};
|
||||
let frame_end: rfx::Block<'_> = decode_cursor(input).map_err(|e| custom_err!("decode frame_end", e))?;
|
||||
if !matches!(frame_end, rfx::Block::CodecChannel(rfx::CodecChannel::FrameEnd(_))) {
|
||||
return Err(general_err!("unexpected block type"));
|
||||
}
|
||||
|
||||
if region.rectangles.is_empty() {
|
||||
region.rectangles = vec![RfxRectangle {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
}];
|
||||
}
|
||||
let region = region;
|
||||
|
||||
trace!(frame_index = frame_begin.index);
|
||||
trace!(destination_rectangle = ?destination);
|
||||
trace!(context = ?self.context);
|
||||
trace!(channels = ?self.channels);
|
||||
trace!(?region);
|
||||
|
||||
let clipping_rectangles = clipping_rectangles(region.rectangles.as_slice(), destination, width, height);
|
||||
trace!("Clipping rectangles: {:?}", clipping_rectangles);
|
||||
|
||||
let mut final_update_rectangle = clipping_rectangles.extents.clone();
|
||||
|
||||
for (update_rectangle, tile_data) in tiles_to_rectangles(tile_set.tiles.as_slice(), destination)
|
||||
.zip(map_tiles_data(tile_set.tiles.as_slice(), tile_set.quants.as_slice()))
|
||||
{
|
||||
decode_tile(
|
||||
&tile_data,
|
||||
entropy_algorithm,
|
||||
self.decoding_tiles.tile_output.as_mut(),
|
||||
self.decoding_tiles.ycbcr_buffer.as_mut(),
|
||||
self.decoding_tiles.ycbcr_temp_buffer.as_mut(),
|
||||
)?;
|
||||
|
||||
let current_update_rectangle = image.apply_tile(
|
||||
&self.decoding_tiles.tile_output,
|
||||
PixelFormat::RgbA32,
|
||||
&clipping_rectangles,
|
||||
&update_rectangle,
|
||||
)?;
|
||||
|
||||
final_update_rectangle = final_update_rectangle.union(¤t_update_rectangle);
|
||||
}
|
||||
|
||||
Ok((frame_begin.index, final_update_rectangle))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct DecodingTileContext {
|
||||
tile_output: Vec<u8>,
|
||||
ycbcr_buffer: Vec<Vec<i16>>,
|
||||
ycbcr_temp_buffer: Vec<i16>,
|
||||
}
|
||||
|
||||
impl DecodingTileContext {
|
||||
fn new() -> Self {
|
||||
let tile_size = usize::from(TILE_SIZE);
|
||||
Self {
|
||||
tile_output: vec![0; tile_size * tile_size * 4],
|
||||
ycbcr_buffer: vec![vec![0; tile_size * tile_size]; 3],
|
||||
ycbcr_temp_buffer: vec![0; tile_size * tile_size],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_tile(
|
||||
tile: &TileData<'_>,
|
||||
entropy_algorithm: EntropyAlgorithm,
|
||||
output: &mut [u8],
|
||||
ycbcr_temp: &mut [Vec<i16>],
|
||||
temp: &mut [i16],
|
||||
) -> SessionResult<()> {
|
||||
for ((quant, data), ycbcr_buffer) in tile.quants.iter().zip(tile.data.iter()).zip(ycbcr_temp.iter_mut()) {
|
||||
decode_component(quant, entropy_algorithm, data, ycbcr_buffer.as_mut_slice(), temp)?;
|
||||
}
|
||||
|
||||
let ycbcr_buffer = YCbCrBuffer {
|
||||
y: ycbcr_temp[0].as_slice(),
|
||||
cb: ycbcr_temp[1].as_slice(),
|
||||
cr: ycbcr_temp[2].as_slice(),
|
||||
};
|
||||
|
||||
color_conversion::ycbcr_to_rgba(ycbcr_buffer, output).map_err(|e| custom_err!("decode_tile", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decode_component(
|
||||
quant: &Quant,
|
||||
entropy_algorithm: EntropyAlgorithm,
|
||||
data: &[u8],
|
||||
output: &mut [i16],
|
||||
temp: &mut [i16],
|
||||
) -> SessionResult<()> {
|
||||
rlgr::decode(entropy_algorithm, data, output).map_err(|e| custom_err!("decode_component", e))?;
|
||||
subband_reconstruction::decode(&mut output[4032..]);
|
||||
quantization::decode(output, quant);
|
||||
dwt::decode(output, temp);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clipping_rectangles(
|
||||
rectangles: &[RfxRectangle],
|
||||
destination: &InclusiveRectangle,
|
||||
width: u16,
|
||||
height: u16,
|
||||
) -> Region {
|
||||
let mut clipping_rectangles = Region::new();
|
||||
|
||||
rectangles
|
||||
.iter()
|
||||
.map(|r| InclusiveRectangle {
|
||||
left: min(destination.left + r.x, width - 1),
|
||||
top: min(destination.top + r.y, height - 1),
|
||||
right: min(destination.left + r.x + r.width - 1, width - 1),
|
||||
bottom: min(destination.top + r.y + r.height - 1, height - 1),
|
||||
})
|
||||
.for_each(|r| clipping_rectangles.union_rectangle(r));
|
||||
|
||||
clipping_rectangles
|
||||
}
|
||||
|
||||
fn tiles_to_rectangles<'a>(
|
||||
tiles: &'a [Tile<'_>],
|
||||
destination: &'a InclusiveRectangle,
|
||||
) -> impl Iterator<Item = InclusiveRectangle> + 'a {
|
||||
tiles.iter().map(|t| InclusiveRectangle {
|
||||
left: destination.left + t.x * TILE_SIZE,
|
||||
top: destination.top + t.y * TILE_SIZE,
|
||||
right: destination.left + t.x * TILE_SIZE + TILE_SIZE - 1,
|
||||
bottom: destination.top + t.y * TILE_SIZE + TILE_SIZE - 1,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_tiles_data<'a>(tiles: &[Tile<'a>], quants: &[Quant]) -> Vec<TileData<'a>> {
|
||||
tiles
|
||||
.iter()
|
||||
.map(|t| TileData {
|
||||
quants: [
|
||||
quants[usize::from(t.y_quant_index)].clone(),
|
||||
quants[usize::from(t.cb_quant_index)].clone(),
|
||||
quants[usize::from(t.cr_quant_index)].clone(),
|
||||
],
|
||||
data: [t.y_data, t.cb_data, t.cr_data],
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct TileData<'a> {
|
||||
quants: [Quant; 3],
|
||||
data: [&'a [u8]; 3],
|
||||
}
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
use ironrdp_core::{WriteBuf, decode};
|
||||
use ironrdp_dvc::{DrdynvcClient, DvcProcessor, DynamicVirtualChannel};
|
||||
use ironrdp_pdu::mcs::{DisconnectProviderUltimatum, DisconnectReason, McsMessage, SendDataIndicationCtx};
|
||||
use ironrdp_pdu::rdp::autodetect::{AutoDetectReqPdu, AutoDetectRequest, AutoDetectResponse, AutoDetectRspPdu};
|
||||
use ironrdp_pdu::rdp::headers::ShareDataPdu;
|
||||
use ironrdp_pdu::rdp::multitransport::MultitransportRequestPdu;
|
||||
use ironrdp_pdu::rdp::server_error_info::{ErrorInfo, ProtocolIndependentCode, ServerSetErrorInfoPdu};
|
||||
use ironrdp_pdu::x224::X224;
|
||||
use ironrdp_svc::{StaticChannelSet, SvcMessage, SvcProcessor, SvcProcessorMessages, client_encode_svc_messages};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{SessionError, SessionErrorExt as _, SessionResult, reason_err};
|
||||
|
||||
/// X224 Processor output
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProcessorOutput {
|
||||
/// A buffer with encoded data to send to the server.
|
||||
ResponseFrame(Vec<u8>),
|
||||
/// A graceful disconnect notification. Client should close the connection upon receiving this.
|
||||
Disconnect(DisconnectDescription),
|
||||
/// Received a [`ironrdp_pdu::rdp::headers::ServerDeactivateAll`] PDU. Client should execute the
|
||||
/// [Deactivation-Reactivation Sequence].
|
||||
///
|
||||
/// [Deactivation-Reactivation Sequence]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dfc234ce-481a-4674-9a5d-2a7bafb14432
|
||||
DeactivateAll,
|
||||
/// Server Initiate Multitransport Request. The application should establish a
|
||||
/// sideband UDP transport using the request ID and security cookie, then send
|
||||
/// a [`MultitransportResponsePdu`] back on the IO channel.
|
||||
///
|
||||
/// See [\[MS-RDPBCGR\] 2.2.15.1].
|
||||
///
|
||||
/// [\[MS-RDPBCGR\] 2.2.15.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/de783158-8b01-4818-8fb0-62523a5b3490
|
||||
/// [`MultitransportResponsePdu`]: ironrdp_pdu::rdp::multitransport::MultitransportResponsePdu
|
||||
MultitransportRequest(MultitransportRequestPdu),
|
||||
/// Auto-detect network characteristics from server ([\[MS-RDPBCGR\] 2.2.14]).
|
||||
///
|
||||
/// Currently only surfaces [`AutoDetectRequest::NetworkCharacteristicsResult`].
|
||||
/// RTT requests are handled internally with automatic responses.
|
||||
///
|
||||
/// [\[MS-RDPBCGR\] 2.2.14]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dc672839-4f4e-40b1-a71c-cd6a959baa38
|
||||
AutoDetect(AutoDetectRequest),
|
||||
/// Slow-path graphics update ([MS-RDPBCGR] 2.2.9.1.1.3).
|
||||
/// Raw update payload starting with `updateType(u16)`.
|
||||
GraphicsUpdate(Vec<u8>),
|
||||
/// Slow-path pointer update ([MS-RDPBCGR] 2.2.9.1.1.4).
|
||||
/// Raw pointer payload starting with `messageType(u16) + pad(u16)`.
|
||||
PointerUpdate(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DisconnectDescription {
|
||||
/// Includes the reason from the MCS Disconnect Provider Ultimatum.
|
||||
/// This is the least-specific disconnect reason and is only used
|
||||
/// when a more specific disconnect code is not available.
|
||||
McsDisconnect(DisconnectReason),
|
||||
|
||||
/// Includes the error information sent by the RDP server when there
|
||||
/// is a connection or disconnection failure.
|
||||
ErrorInfo(ErrorInfo),
|
||||
}
|
||||
|
||||
pub struct Processor {
|
||||
static_channels: StaticChannelSet,
|
||||
user_channel_id: u16,
|
||||
io_channel_id: u16,
|
||||
message_channel_id: Option<u16>,
|
||||
share_id: u32,
|
||||
}
|
||||
|
||||
impl Processor {
|
||||
pub fn new(
|
||||
static_channels: StaticChannelSet,
|
||||
user_channel_id: u16,
|
||||
io_channel_id: u16,
|
||||
message_channel_id: Option<u16>,
|
||||
share_id: u32,
|
||||
) -> Self {
|
||||
Self {
|
||||
static_channels,
|
||||
user_channel_id,
|
||||
io_channel_id,
|
||||
message_channel_id,
|
||||
share_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_share_id(&mut self, share_id: u32) {
|
||||
self.share_id = share_id;
|
||||
}
|
||||
|
||||
pub fn get_svc_processor<T: SvcProcessor + 'static>(&self) -> Option<&T> {
|
||||
self.static_channels
|
||||
.get_by_type::<T>()
|
||||
.and_then(|svc| svc.channel_processor_downcast_ref())
|
||||
}
|
||||
|
||||
pub fn get_svc_processor_mut<T: SvcProcessor + 'static>(&mut self) -> Option<&mut T> {
|
||||
self.static_channels
|
||||
.get_by_type_mut::<T>()
|
||||
.and_then(|svc| svc.channel_processor_downcast_mut())
|
||||
}
|
||||
|
||||
/// Completes user's SVC request with data, required to sent it over the network and returns
|
||||
/// a buffer with encoded data.
|
||||
pub fn process_svc_processor_messages<C: SvcProcessor + 'static>(
|
||||
&self,
|
||||
messages: SvcProcessorMessages<C>,
|
||||
) -> SessionResult<Vec<u8>> {
|
||||
let channel_id = self
|
||||
.static_channels
|
||||
.get_channel_id_by_type::<C>()
|
||||
.ok_or_else(|| reason_err!("SVC", "channel not found"))?;
|
||||
|
||||
process_svc_messages(messages.into(), channel_id, self.user_channel_id)
|
||||
}
|
||||
|
||||
pub fn get_dvc<T: DvcProcessor + 'static>(&self) -> Option<&DynamicVirtualChannel> {
|
||||
self.get_svc_processor::<DrdynvcClient>()?.get_dvc_by_type_id::<T>()
|
||||
}
|
||||
|
||||
pub fn get_dvc_by_channel_id(&self, channel_id: u32) -> Option<&DynamicVirtualChannel> {
|
||||
self.get_svc_processor::<DrdynvcClient>()?
|
||||
.get_dvc_by_channel_id(channel_id)
|
||||
}
|
||||
|
||||
/// Processes a received PDU. Returns a vector of [`ProcessorOutput`] that must be processed
|
||||
/// in the returned order.
|
||||
pub fn process(&mut self, frame: &[u8]) -> SessionResult<Vec<ProcessorOutput>> {
|
||||
let data_ctx: SendDataIndicationCtx<'_> =
|
||||
ironrdp_pdu::mcs::decode_send_data_indication(frame).map_err(SessionError::decode)?;
|
||||
let channel_id = data_ctx.channel_id;
|
||||
|
||||
if channel_id == self.io_channel_id {
|
||||
self.process_io_channel(data_ctx)
|
||||
} else if self.message_channel_id == Some(channel_id) {
|
||||
self.process_message_channel(data_ctx)
|
||||
} else if let Some(svc) = self.static_channels.get_by_channel_id_mut(channel_id) {
|
||||
let response_pdus = svc.process(data_ctx.user_data).map_err(SessionError::pdu)?;
|
||||
process_svc_messages(response_pdus, channel_id, data_ctx.initiator_id)
|
||||
.map(|data| vec![ProcessorOutput::ResponseFrame(data)])
|
||||
} else {
|
||||
Err(reason_err!("X224", "unexpected channel received: ID {channel_id}"))
|
||||
}
|
||||
}
|
||||
|
||||
fn process_io_channel(&self, data_ctx: SendDataIndicationCtx<'_>) -> SessionResult<Vec<ProcessorOutput>> {
|
||||
debug_assert_eq!(data_ctx.channel_id, self.io_channel_id);
|
||||
|
||||
let io_channel = ironrdp_pdu::rdp::headers::decode_io_channel(data_ctx).map_err(SessionError::decode)?;
|
||||
|
||||
match io_channel {
|
||||
ironrdp_pdu::rdp::headers::IoChannelPdu::Data(ctx) => {
|
||||
match ctx.pdu {
|
||||
ShareDataPdu::SaveSessionInfo(session_info) => {
|
||||
debug!("Got Session Save Info PDU: {session_info:?}");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
// FIXME: workaround fix to not terminate the session on "unhandled PDU: Set Keyboard Indicators PDU"
|
||||
ShareDataPdu::SetKeyboardIndicators(data) => {
|
||||
debug!("Got Keyboard Indicators PDU: {data:?}");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu(ErrorInfo::ProtocolIndependentCode(
|
||||
ProtocolIndependentCode::None,
|
||||
))) => {
|
||||
debug!("Received None server error");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu(e)) => {
|
||||
// This is a part of server-side graceful disconnect procedure defined
|
||||
// in [MS-RDPBCGR].
|
||||
//
|
||||
// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/149070b0-ecec-4c20-af03-934bbc48adb8
|
||||
let desc = DisconnectDescription::ErrorInfo(e);
|
||||
Ok(vec![ProcessorOutput::Disconnect(desc)])
|
||||
}
|
||||
ShareDataPdu::ShutdownDenied => {
|
||||
debug!("ShutdownDenied received, session will be closed");
|
||||
|
||||
// As defined in [MS-RDPBCGR], when `ShareDataPdu::ShutdownDenied` is received, we
|
||||
// need to send a disconnect ultimatum to the server if we want to proceed with the
|
||||
// session shutdown.
|
||||
//
|
||||
// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68
|
||||
let ultimatum = McsMessage::DisconnectProviderUltimatum(
|
||||
DisconnectProviderUltimatum::from_reason(DisconnectReason::UserRequested),
|
||||
);
|
||||
|
||||
let encoded_pdu = ironrdp_core::encode_vec(&X224(ultimatum)).map_err(SessionError::encode);
|
||||
|
||||
Ok(vec![
|
||||
ProcessorOutput::ResponseFrame(encoded_pdu?),
|
||||
ProcessorOutput::Disconnect(DisconnectDescription::McsDisconnect(
|
||||
DisconnectReason::UserRequested,
|
||||
)),
|
||||
])
|
||||
}
|
||||
// TODO: slow-path payloads may be bulk-compressed when
|
||||
// ClientInfoFlags::COMPRESSION is negotiated. Decompression
|
||||
// should happen here before passing data downstream. Currently
|
||||
// IronRDP does not wire bulk decompression into this path.
|
||||
// FIXME: until this is wired, the client deliberately defaults to the simple,
|
||||
// stateless-friendly MPPC 64K (RDP5) compression level rather than XCRUSH; a
|
||||
// stateful codec would risk silent corruption on slow-path updates.
|
||||
ShareDataPdu::Update(data) => {
|
||||
debug!("Got slow-path graphics update ({} bytes)", data.len());
|
||||
Ok(vec![ProcessorOutput::GraphicsUpdate(data)])
|
||||
}
|
||||
ShareDataPdu::Pointer(data) => {
|
||||
debug!("Got slow-path pointer update ({} bytes)", data.len());
|
||||
Ok(vec![ProcessorOutput::PointerUpdate(data)])
|
||||
}
|
||||
_ => Err(reason_err!(
|
||||
"IO channel",
|
||||
"unhandled PDU: {:?}",
|
||||
ctx.pdu.as_short_name()
|
||||
)),
|
||||
}
|
||||
}
|
||||
ironrdp_pdu::rdp::headers::IoChannelPdu::MultitransportRequest(pdu) => {
|
||||
debug!(
|
||||
"Received Initiate Multitransport Request: request_id={}",
|
||||
pdu.request_id
|
||||
);
|
||||
Ok(vec![ProcessorOutput::MultitransportRequest(pdu)])
|
||||
}
|
||||
ironrdp_pdu::rdp::headers::IoChannelPdu::DeactivateAll(_) => Ok(vec![ProcessorOutput::DeactivateAll]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Process an auto-detect request received on the MCS message channel.
|
||||
///
|
||||
/// During continuous auto-detection ([MS-RDPBCGR] 2.2.14) the server sends
|
||||
/// RTT (and bandwidth) requests on the message channel; the client answers
|
||||
/// RTT requests and surfaces the final Network Characteristics Result.
|
||||
fn process_message_channel(&self, data_ctx: SendDataIndicationCtx<'_>) -> SessionResult<Vec<ProcessorOutput>> {
|
||||
let Some(message_channel_id) = self.message_channel_id else {
|
||||
return Err(reason_err!("message channel", "no message channel negotiated"));
|
||||
};
|
||||
|
||||
let req = decode::<AutoDetectReqPdu>(data_ctx.user_data).map_err(SessionError::decode)?;
|
||||
|
||||
match req.request {
|
||||
AutoDetectRequest::RttRequest { sequence_number, .. } => {
|
||||
let response = AutoDetectRspPdu::new(AutoDetectResponse::RttResponse { sequence_number });
|
||||
let mut frame = WriteBuf::new();
|
||||
ironrdp_pdu::mcs::encode_send_data_request(
|
||||
self.user_channel_id,
|
||||
message_channel_id,
|
||||
&response,
|
||||
&mut frame,
|
||||
)
|
||||
.map_err(SessionError::encode)?;
|
||||
debug!(sequence_number, "Responded to auto-detect RTT request");
|
||||
Ok(vec![ProcessorOutput::ResponseFrame(frame.into_inner())])
|
||||
}
|
||||
req @ AutoDetectRequest::NetworkCharacteristicsResult { .. } => {
|
||||
debug!(?req, "Received network characteristics from server");
|
||||
Ok(vec![ProcessorOutput::AutoDetect(req)])
|
||||
}
|
||||
req => {
|
||||
debug!(?req, "Auto-detect request not yet implemented");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a pdu on the static global channel. Typically used to send input events
|
||||
pub fn encode_static(&self, output: &mut WriteBuf, pdu: ShareDataPdu) -> SessionResult<usize> {
|
||||
let written = ironrdp_pdu::rdp::headers::encode_share_data(
|
||||
self.user_channel_id,
|
||||
self.io_channel_id,
|
||||
self.share_id,
|
||||
pdu,
|
||||
output,
|
||||
)
|
||||
.map_err(SessionError::encode)?;
|
||||
Ok(written)
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes a vector of [`SvcMessage`] in preparation for sending them to the server on the `channel_id` channel.
|
||||
///
|
||||
/// This includes chunkifying the messages, adding MCS, x224, and tpkt headers, and encoding them into a buffer.
|
||||
/// The messages returned here are ready to be sent to the server.
|
||||
///
|
||||
/// The caller is responsible for ensuring that the `channel_id` corresponds to the correct channel.
|
||||
fn process_svc_messages(messages: Vec<SvcMessage>, channel_id: u16, initiator_id: u16) -> SessionResult<Vec<u8>> {
|
||||
client_encode_svc_messages(messages, channel_id, initiator_id).map_err(SessionError::encode)
|
||||
}
|
||||
Vendored
+380
@@ -0,0 +1,380 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [[7.0.0-rc.25](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.24...picky-v7.0.0-rc.25)] - 2026-06-26
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Align RustCrypto ECC dependency versions and remove unnecessary transitive pins ([#506](https://github.com/Devolutions/picky-rs/issues/506)) ([a4346f2853](https://github.com/Devolutions/picky-rs/commit/a4346f2853e726eb05b1fb5de2fa9351c8c44e2e))
|
||||
|
||||
|
||||
|
||||
## [[7.0.0-rc.24](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.23...picky-v7.0.0-rc.24)] - 2026-06-19
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Bump sha3 from 0.11.0 to 0.12.0 ([#495](https://github.com/Devolutions/picky-rs/issues/495)) ([0b065a59fb](https://github.com/Devolutions/picky-rs/commit/0b065a59fb1e65c354084016166a7822ab10f7bf))
|
||||
|
||||
- Advance RustCrypto pins to the current rc.18/rc.33/rc.10 cluster ([#501](https://github.com/Devolutions/picky-rs/issues/501)) ([7d4b2675b9](https://github.com/Devolutions/picky-rs/commit/7d4b2675b91f7803905d972501130fc09d42ce1c))
|
||||
|
||||
## [[7.0.0-rc.23](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.22...picky-v7.0.0-rc.23)] - 2026-04-21
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Upgrade RustCrypto dependencies ([#476](https://github.com/Devolutions/picky-rs/issues/476)) ([8ce838e347](https://github.com/Devolutions/picky-rs/commit/8ce838e3470ac605c912f61dc67c1c6d388b140d))
|
||||
|
||||
## [[7.0.0-rc.22](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.21...picky-v7.0.0-rc.22)] - 2026-02-27
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Bump `sha1` to v0.11.0-rc.3 ([#463](https://github.com/Devolutions/picky-rs/issues/463)) ([e40aee72bd](https://github.com/Devolutions/picky-rs/commit/e40aee72bd248bda84e6fe9e84a648784a6374ed))
|
||||
|
||||
## [[7.0.0-rc.21](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.20...picky-v7.0.0-rc.21)] - 2026-02-02
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Update crypto dependencies ([#448](https://github.com/Devolutions/picky-rs/issues/448)) ([0bf42c1c4b](https://github.com/Devolutions/picky-rs/commit/0bf42c1c4bd727eaf5b4b0a877e8698986e0bd37))
|
||||
|
||||
- Remove transitive pem-rfc7468 crate ([#449](https://github.com/Devolutions/picky-rs/issues/449)) ([cc4caf455d](https://github.com/Devolutions/picky-rs/commit/cc4caf455defd38ef37fc15434b173275937d534))
|
||||
|
||||
- Bump reqwest from 0.12.24 to 0.13.1 ([#453](https://github.com/Devolutions/picky-rs/issues/453)) ([9cbaab9395](https://github.com/Devolutions/picky-rs/commit/9cbaab93955f2fef8f96a589b22e2582d4a8cbf0))
|
||||
|
||||
## [[7.0.0-rc.20](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.19...picky-v7.0.0-rc.20)] - 2025-11-07
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Pin leftover pre-release crypto crates ([#430](https://github.com/Devolutions/picky-rs/issues/430)) ([0fe43dae7b](https://github.com/Devolutions/picky-rs/commit/0fe43dae7b463a16d48b00b08ca6ce21a013af74))
|
||||
|
||||
The patch version upgrade is not allowed to bring breaking changes, but
|
||||
this rule doesn't work for an `rc` version. So we should pin the `rc`
|
||||
versions, to not allow _cargo_ update to a new `rc` automatically.
|
||||
|
||||
## [[7.0.0-rc.19](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.18...picky-v7.0.0-rc.19)] - 2025-10-10
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- Support parsing certs with MLDSA public keys ([#411](https://github.com/Devolutions/picky-rs/issues/411)) ([abc63dde88](https://github.com/Devolutions/picky-rs/commit/abc63dde88360e35ef225cfd1c279958f47c3abc))
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Pin RustCrypto release candidate crates ([#417](https://github.com/Devolutions/picky-rs/issues/417)) ([8a79282bbc](https://github.com/Devolutions/picky-rs/commit/8a79282bbc0dae9df222f16d261b7dd1f03cd66f))
|
||||
|
||||
## [[7.0.0-rc.18](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.17...picky-v7.0.0-rc.18)] - 2025-09-26
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Bump the crypto group across 1 directory with 3 updates (#388) ([58d179a0c3](https://github.com/Devolutions/picky-rs/commit/58d179a0c39d701025a363c3f294912c2881a8f5))
|
||||
|
||||
## [[7.0.0-rc.17](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.16...picky-v7.0.0-rc.17)] - 2025-08-18
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Bump rstest from 0.25.0 to 0.26.1 (#391) ([2a169deba2](https://github.com/Devolutions/picky-rs/commit/2a169deba2ba177685d58550f4214f59f25ebec0))
|
||||
|
||||
## [[7.0.0-rc.16](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.15...picky-v7.0.0-rc.16)] - 2025-07-15
|
||||
|
||||
### <!-- 0 -->Security
|
||||
|
||||
- Reject issuer certificates missing basic constraints (#385) ([4d8be23912](https://github.com/Devolutions/picky-rs/commit/4d8be239126cd34843a74de017118823699b8aa7))
|
||||
|
||||
As per RFC 5280:
|
||||
|
||||
> The cA boolean indicates whether the certified public key may be used
|
||||
> to verify certificate signatures. If the cA boolean is not asserted,
|
||||
> then the keyCertSign bit in the key usage extension MUST NOT be
|
||||
> asserted. If the basic constraints extension is not present in a
|
||||
> version 3 certificate, or the extension is present but the cA boolean
|
||||
> is not asserted, then the certified public key MUST NOT be used to
|
||||
> verify certificate signatures.
|
||||
|
||||
## [[7.0.0-rc.15](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.14...picky-v7.0.0-rc.15)] - 2025-06-24
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Bump MSVC to 1.85 (#379) ([302df9912d](https://github.com/Devolutions/picky-rs/commit/302df9912d6f038f21b64fc916bd82836944c382))
|
||||
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [[7.0.0-rc.14](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.13...picky-v7.0.0-rc.14)] - 2025-05-14
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- Add HttpSignature::to_signing_string() method ([fe054c47f0](https://github.com/Devolutions/picky-rs/commit/fe054c47f04920383d93371bda948a961cbefdfd))
|
||||
|
||||
Equivalent to HttpSignature::to_string().
|
||||
|
||||
## [[7.0.0-rc.13](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.12...picky-v7.0.0-rc.13)] - 2025-03-03
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- [**breaking**] Comply with draft-cavage-http-signatures-12 when signing HTTP messages (#351) ([f043d04e14](https://github.com/Devolutions/picky-rs/commit/f043d04e1462cf0eddd9140f22a4d364cb3ad9cd))
|
||||
|
||||
Change request target handling to comply with draft-cavage-http-signatures-12 when signing HTTP messages.
|
||||
|
||||
## [[7.0.0-rc.12](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.11...picky-v7.0.0-rc.12)] - 2025-01-16
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Symlinks to license files in packages (#339) ([1834c04f39](https://github.com/Devolutions/picky-rs/commit/1834c04f3930fb1bbf040deb6525b166e378b8aa))
|
||||
|
||||
Use symlinks instead of copying files to avoid a “dirty” state during
|
||||
cargo publish and preserve VCS info. With #337 merged, CI handles
|
||||
publishing consistently, so developer environments no longer matter.
|
||||
|
||||
|
||||
## [7.0.0-rc.1 to 7.0.0-rc.11](https://github.com/Devolutions/picky-rs/compare/picky-6.4.0...picky-v7.0.0-rc.11)
|
||||
|
||||
### Added
|
||||
|
||||
- Support for ECDSA p256 p384 and p521 signatures
|
||||
- Support for MD5 hashing
|
||||
- CTL implementation behind `ctl` feature
|
||||
- CTL fetching over HTTP is behind `ctl_http_fetch` feature
|
||||
- `Pkcs7::digest_algorithms`
|
||||
- `Pkcs7::signer_infos`
|
||||
- `Pkcs7::encapsulated_content_info`
|
||||
- `Pkcs7::decode_certificates`
|
||||
- `impl From<Pkcs7Certificate> for Pkcs7`
|
||||
- `impl From<Pkcs7> for Pkcs7Certificate`
|
||||
- `AuthenticodeSignature` struct
|
||||
- `AuthenticodeSignature::new`
|
||||
- `AuthenticodeSignature::from_der`
|
||||
- `AuthenticodeSignature::from_pem`
|
||||
- `AuthenticodeSignature::from_pem_str`
|
||||
- `AuthenticodeSignature::to_der`
|
||||
- `AuthenticodeSignature::to_pem`
|
||||
- `AuthenticodeSignature::signing_certificate`
|
||||
- `AuthenticodeSignature::authenticode_verifier`
|
||||
- `AuthenticodeSignature::file_hash`
|
||||
- `Authenticated_attributes::authenticated_attributes`
|
||||
- `Authenticated_attributes::unauthenticated_attributes`
|
||||
- `impl From<Pkcs7> for AuthenticodeSignature`
|
||||
- `From<AuthenticodeSignature> for Pkcs7`
|
||||
- Authenticode validation
|
||||
- Support for `AuthenticodeSignature` timestamping:
|
||||
- Method `timestamp` to `AuthenticodeSignature`
|
||||
- `Timestamper` trait.
|
||||
- Timestamping implementation using reqwest is behind `http_timestamp` feature
|
||||
- Authenticode timestamp request struct - `TimestampRequest`
|
||||
- `AuthenticodeBuilder` for easier `AuthenticodeSignature` creation
|
||||
- `SignatureAlgorithm::hash_algorithm`
|
||||
- Support for `time 0.3` types conversions behind `time_conversion` feature gate
|
||||
- `PrivateKey::to_pem_str`
|
||||
- `PrivateKey::from_ec_components`
|
||||
- `PrivateKey::from_ec_der_with_curve`
|
||||
- `PrivateKey::from_ec_der_with_curve_oid`
|
||||
- `PrivateKey::generate_ec`
|
||||
- `PrivateKey::generate_ed`
|
||||
- `PrivateKey::from_ec_encoded_components`
|
||||
- `PrivateKey::from_ed_encoded_components`
|
||||
- `PublicKey::to_pem_str`
|
||||
- Support SSH keys and certificates
|
||||
- `CheckedJwtEnc::new_with_cty`
|
||||
- `CheckedJwtSig::new_with_cty`
|
||||
- Support for JWT additional header parameters
|
||||
- Support for EC private/public keys
|
||||
- JWS and JWK EC curves support
|
||||
- EC curves support for SSH keys and certificates
|
||||
- EC x509 certificates verification/signing
|
||||
- `JwkKeyType::new_ec_key`
|
||||
- `JwkKeyType::new_ed_key`
|
||||
- `JwkKeyType::as_ec`
|
||||
- `JwkKeyType::as_ed`
|
||||
- `JwkKeyType::is_ec`
|
||||
- `JwkKeyType::is_ed`
|
||||
- `SshPrivateKey::generate_ec`
|
||||
- `SshPrivateKey::generate_ed`
|
||||
- `JwkEcPublicKeyCurve` enum
|
||||
- `JwkEdPublicKeyAlgorithm` enum
|
||||
- `JwsAlg::EdDSA` variant
|
||||
- `JwsAlg::ED25519` variant (`#[deprecated]` from the start, just for compatibility with other libs)
|
||||
- Support for X25519 and Ed25519 public/private keys
|
||||
- JWS and JWK Ed25519 keys/signing support
|
||||
- Ed25519 support for SSH keys and certificates
|
||||
- Ed25519 x509 certificates verification/signing
|
||||
- Support for `ECDH-ES` JWE algorithm (P256, P384 and P521 EC keys, X25519 ED keys)
|
||||
- Support for `PKCS12`(PFX) files parsing/building
|
||||
- PuTTY PPK key format support
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump minimal rustc version to 1.70
|
||||
- Updated `p256`/`p384` to latest version
|
||||
- (Breaking) Move Authenticode related code from `picky::x509::wincert` to `picky::x509::pkcs7::authenticode` module
|
||||
- (Breaking) Authenticode implementation is now behind `pkcs7` feature
|
||||
- (Breaking) `PrivateKey::to_pem` and `PublicKey::to_pem` now return a `Pem`
|
||||
- (Breaking) Separate JWT validation from decoding in JOSE API (this makes API more convenient to first process header
|
||||
and then decide what kind of validation should be applied, or what claims type to deserialize into)
|
||||
- (Breaking) `PrivateKey` now have private struct representation instead of open enum
|
||||
- (Breaking) Now all `PrivateKey::to_public` and all related APIs for private keys return `Result`
|
||||
instead of plain `PublicKey`. This change was required because added support for EC/ED keys is not
|
||||
infallible in regard to public key extraction (public key could be missing from the file and could
|
||||
not be generated because of an unsupported algorithm/curve)
|
||||
- "picky-test-data" is a new dev-dependency.
|
||||
|
||||
### Removed
|
||||
|
||||
- (Breaking) `ec` non-default cargo feature (EC operations now enabled by default)
|
||||
|
||||
### Fixed
|
||||
|
||||
- `BufReader` panic in `WinCertificate::decode` and `WinCertificate::encode` if data len is bigger than default capacity.
|
||||
- `WinCertificate` encoding: `length` wasn’t correct.
|
||||
- Leading zeros in JWK encoding.
|
||||
|
||||
JWK encoding of a value is the unsigned big-endian representation as an octet sequence.
|
||||
The octet sequence MUST utilize the minimum number of octets needed to represent the value.
|
||||
That is: **no leading zero** must be present.
|
||||
|
||||
See issue [#140](https://github.com/Devolutions/picky-rs/issues/140)
|
||||
|
||||
- Fetch curve info from private_key_algorithm ([#143](https://github.com/Devolutions/picky-rs/issues/143))
|
||||
|
||||
Pick curve info from private_key_algorithm field.
|
||||
|
||||
- Missing `zeroize` on drop of internal `EcdsaKeypair` structure
|
||||
- Fixed invalid padding logic of encrypted SSH keys
|
||||
- Fixed invalid generated SSH EC keys
|
||||
|
||||
### Removed
|
||||
|
||||
- (Breaking) `Jwt::new_encrypted`
|
||||
- (Breaking) `Jwt::new_signed`
|
||||
|
||||
## [6.4.0](https://github.com/Devolutions/picky-rs/compare/picky-6.3.0...picky-6.4.0) – 2021-08-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump minimum supported Rust version to 1.51
|
||||
- Update `rsa` dependency to `0.5`
|
||||
- Update `picky-asn1` dependency to `0.4`
|
||||
- More robust certification validation (see commit [f5f8cb60e41](https://github.com/Devolutions/picky-rs/commit/f5f8cb60e410ffe49aabace131f7b802e206ced0) for details)
|
||||
|
||||
## [6.3.0](https://github.com/Devolutions/picky-rs/compare/picky-6.2.0...picky-6.3.0) – 2021-05-27
|
||||
|
||||
### Added
|
||||
|
||||
- PKCS7 implementation behind `pkcs7` feature
|
||||
- `WinCertificate` for authenticode behind `wincert` feature
|
||||
|
||||
### Changed
|
||||
|
||||
- Update `aes-gcm` dependency to `0.9`
|
||||
|
||||
## [6.2.0](https://github.com/Devolutions/picky-rs/compare/picky-6.1.1...picky-6.2.0) – 2021-03-04
|
||||
|
||||
### Added
|
||||
|
||||
- `Csr::generate_with_attributes` to generate CSR with attributes.
|
||||
- `CertificateBuilder::inherit_extensions_from_csr_attributes` to allow certificate to inherit extensions requested by CSR attribute.
|
||||
- Various API additions to `GeneralNames` for improved ergonomics.
|
||||
|
||||
## 6.1.2 – 2021-01-11
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix bad `use`s statements to `serde::export`
|
||||
|
||||
## 6.1.1 – 2020-12-11
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix `HttpSignatureError`'s Display trait implementation (`certificate expired` → `signature expired`).
|
||||
- Fix certificate validity period that MUST be encoded as `UTCTime` through the year 2049 as per RFC 5280.
|
||||
Previously, they were always encoded as `GeneralizedTime`.
|
||||
|
||||
## 6.1.0 – 2020-10-21
|
||||
|
||||
### Added
|
||||
|
||||
- `CertificateBuilder::serial_number` can be used to provide a custom serial number instead of generating one.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bad generation for second exponent (`pq`) when generating PKCS#8 structure.
|
||||
- Serial number was sometimes generated as negative.
|
||||
|
||||
## 6.0.0 – 2020-10-13
|
||||
|
||||
### Added
|
||||
|
||||
- Implementation of `Jwe` (JSON Web Encryption) RFC.
|
||||
- Email attribute can be added to `DirectoryName` using `add_email` method.
|
||||
- `from_pem_str` method to `Cert`, `Csr`, `PrivateKey` and `PublicKey`.
|
||||
|
||||
### Changed
|
||||
|
||||
- Separate `SignatureHashType` into two separate enums `HashAlgorithm` and `SignatureAlgorithm`.
|
||||
- `KeyIdGenError` is now wrapping a `HashAlgorithm`.
|
||||
- Update `rsa` dependency.
|
||||
- `HttpSignature` store custom `algorithm` names instead of just ignoring them.
|
||||
- Major `jose` module changes
|
||||
- JOSE header fields are renamed to be identical to RFCs when possible.
|
||||
- `Jwt` is now divided into `Jws` (JSON Web Signature) and `Jwe` (JSON Web Encryption)
|
||||
- `Jws` provides an API to sign any kind of data (binary). JSON claims are part of `Jwt` only.
|
||||
- `Jwe` provides an API to encrypt any kind of data (binary). JSON claims are part of `Jwt` only.
|
||||
- Typo in `CertificateBuilder` API: `valididy` has been renamed to `validity`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- RSA private key generation ([#53](https://github.com/Devolutions/picky-rs/issues/53)).
|
||||
|
||||
## 5.1.1 – 2020-07-13
|
||||
|
||||
### Changed
|
||||
|
||||
- Better `CaChainError::AuthorityKeyIdMismatch` display.
|
||||
|
||||
## 5.1.0 – 2020-07-07
|
||||
|
||||
### Added
|
||||
|
||||
- Add `BufRead`-based API to read PEM streams: `pem::Pem::read_from` and `pem::read_pem`.
|
||||
|
||||
### Changed
|
||||
|
||||
- Some internal types are moved to a new `picky_asn1_x509` crate but API is unchanged.
|
||||
- Dependencies clean up.
|
||||
|
||||
## 5.0.0 – 2020-05-06
|
||||
|
||||
### Added
|
||||
|
||||
- Add `into_public_key` method to `x509::Cert`.
|
||||
- Support for the ["algorithm" http signature parameter](https://tools.ietf.org/html/draft-cavage-http-signatures-12#section-2.1.3).
|
||||
|
||||
### Changed
|
||||
|
||||
- `Jwt` has no generic lifetime parameter anymore.
|
||||
|
||||
### Removed
|
||||
|
||||
- `Cert::verify` and `Cert::verify_chain` methods (previously deprecated)
|
||||
- `HttpRequest` trait impl for `http` crate v0.1
|
||||
|
||||
### Fixed
|
||||
|
||||
- Add missing parameters for AES and SHA object identifiers ([668c06e8d](https://github.com/Devolutions/picky-rs/commit/668c06e8d8e8a0caae8bd13cf81c189bbc2e4918))
|
||||
|
||||
## 4.7.0 – 2020-04-16
|
||||
|
||||
### Added
|
||||
|
||||
- Implement `From<UTCDate>` trait on `DateTime<Utc>`.
|
||||
- Support for leeway in HTTP signature verifier.
|
||||
- Verifier API to X509 Cert.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- `Cert::verify` and `Cert::verify_chain` methods in favor of the `Cert::verifier` method.
|
||||
+2564
File diff suppressed because it is too large
Load Diff
Vendored
+353
@@ -0,0 +1,353 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2024"
|
||||
rust-version = "1.85"
|
||||
name = "picky"
|
||||
version = "7.0.0-rc.25"
|
||||
authors = [
|
||||
"Benoît CORTIER <bcortier@devolutions.net>",
|
||||
"Jonathan Trepanier <jtrepanier@devolutions.net>",
|
||||
"François Dubois <fdubois@devolutions.net>",
|
||||
"Richard Markiewicz <rmarkiewicz@devolutions.net>",
|
||||
"Ionut Mihalcea <ionut.mihalcea@arm.com>",
|
||||
"Kim Altintop <kim@monadic.xyz>",
|
||||
"Alexandr Yusuk <aleksandr.yusuk@apriorit.com>",
|
||||
"Flavio Castelli <flavio@castelli.me>",
|
||||
"Xynnn_ <xynnn@linux.alibaba.com>",
|
||||
]
|
||||
build = false
|
||||
include = [
|
||||
"src/**/*",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"LICENSE-*",
|
||||
]
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "Portable X.509, PKI, JOSE and HTTP signature implementation."
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"x509",
|
||||
"jwt",
|
||||
"signature",
|
||||
"jose",
|
||||
"pki",
|
||||
]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/Devolutions/picky-rs"
|
||||
resolver = "2"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[features]
|
||||
chrono_conversion = [
|
||||
"dep:chrono",
|
||||
"picky-asn1/chrono_conversion",
|
||||
]
|
||||
ctl = [
|
||||
"picky-asn1-x509/ctl",
|
||||
"pkcs7",
|
||||
"chrono_conversion",
|
||||
]
|
||||
ctl_http_fetch = [
|
||||
"dep:reqwest",
|
||||
"dep:cab",
|
||||
"ctl",
|
||||
]
|
||||
default = [
|
||||
"x509",
|
||||
"jose",
|
||||
"http_signature",
|
||||
"http_trait_impl",
|
||||
"pkcs12",
|
||||
]
|
||||
http_signature = []
|
||||
http_timestamp = ["dep:reqwest"]
|
||||
http_trait_impl = ["dep:http"]
|
||||
jose = [
|
||||
"dep:serde_json",
|
||||
"dep:aes-gcm",
|
||||
"dep:ctr",
|
||||
"dep:cbc",
|
||||
"dep:aes-kw",
|
||||
"dep:aes",
|
||||
]
|
||||
pkcs12 = [
|
||||
"picky-asn1-x509/pkcs12",
|
||||
"dep:des",
|
||||
"dep:rc2",
|
||||
"dep:cbc",
|
||||
"dep:hmac",
|
||||
"dep:aes",
|
||||
"x509",
|
||||
"dep:pbkdf2",
|
||||
]
|
||||
pkcs7 = [
|
||||
"x509",
|
||||
"picky-asn1-x509/pkcs7",
|
||||
]
|
||||
putty = [
|
||||
"dep:argon2",
|
||||
"dep:hmac",
|
||||
"ssh",
|
||||
"dep:blake2",
|
||||
]
|
||||
ssh = [
|
||||
"dep:byteorder",
|
||||
"dep:aes",
|
||||
"dep:ctr",
|
||||
"dep:cbc",
|
||||
"dep:bcrypt-pbkdf",
|
||||
"dep:lexical-sort",
|
||||
"dep:hmac",
|
||||
"dep:pbkdf2",
|
||||
]
|
||||
time_conversion = [
|
||||
"dep:time",
|
||||
"picky-asn1/time_conversion",
|
||||
]
|
||||
wincert = [
|
||||
"x509",
|
||||
"dep:byteorder",
|
||||
]
|
||||
x509 = []
|
||||
|
||||
[lib]
|
||||
name = "picky"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies.aes]
|
||||
version = "0.9"
|
||||
optional = true
|
||||
|
||||
[dependencies.aes-gcm]
|
||||
version = "^0.11.0-rc.4"
|
||||
optional = true
|
||||
|
||||
[dependencies.aes-kw]
|
||||
version = "0.3"
|
||||
optional = true
|
||||
|
||||
[dependencies.argon2]
|
||||
version = "^0.6.0-rc.8"
|
||||
optional = true
|
||||
|
||||
[dependencies.base64]
|
||||
version = "0.22"
|
||||
|
||||
[dependencies.bcrypt-pbkdf]
|
||||
version = "0.11"
|
||||
optional = true
|
||||
|
||||
[dependencies.blake2]
|
||||
version = "^0.11.0-rc.6"
|
||||
optional = true
|
||||
|
||||
[dependencies.byteorder]
|
||||
version = "1.5"
|
||||
optional = true
|
||||
|
||||
[dependencies.cab]
|
||||
version = "0.6"
|
||||
optional = true
|
||||
|
||||
[dependencies.cbc]
|
||||
version = "0.2"
|
||||
features = ["alloc"]
|
||||
optional = true
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4"
|
||||
features = ["clock"]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.crypto-bigint]
|
||||
version = "0.7"
|
||||
|
||||
[dependencies.crypto-common]
|
||||
version = "0.2"
|
||||
|
||||
[dependencies.ctr]
|
||||
version = "0.10"
|
||||
optional = true
|
||||
|
||||
[dependencies.curve25519-dalek]
|
||||
version = "^5.0.0-rc.1"
|
||||
|
||||
[dependencies.des]
|
||||
version = "0.9"
|
||||
optional = true
|
||||
|
||||
[dependencies.digest]
|
||||
version = "0.11"
|
||||
|
||||
[dependencies.ecdsa]
|
||||
version = "^0.17.0-rc.22"
|
||||
|
||||
[dependencies.ed25519-dalek]
|
||||
version = "^3.0.0-rc.1"
|
||||
features = [
|
||||
"hazmat",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[dependencies.hex]
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.hmac]
|
||||
version = "0.13"
|
||||
optional = true
|
||||
|
||||
[dependencies.http]
|
||||
version = "1.4"
|
||||
optional = true
|
||||
|
||||
[dependencies.inout]
|
||||
version = "0.2.2"
|
||||
|
||||
[dependencies.lexical-sort]
|
||||
version = "0.3"
|
||||
optional = true
|
||||
|
||||
[dependencies.md5]
|
||||
version = "0.11"
|
||||
features = ["oid"]
|
||||
package = "md-5"
|
||||
|
||||
[dependencies.p256]
|
||||
version = "^0.14.0-rc.14"
|
||||
features = ["ecdh"]
|
||||
|
||||
[dependencies.p384]
|
||||
version = "^0.14.0-rc.14"
|
||||
features = ["ecdh"]
|
||||
|
||||
[dependencies.p521]
|
||||
version = "^0.14.0-rc.14"
|
||||
features = ["ecdh"]
|
||||
|
||||
[dependencies.pbkdf2]
|
||||
version = "0.13"
|
||||
optional = true
|
||||
|
||||
[dependencies.picky-asn1]
|
||||
version = "0.10"
|
||||
features = ["zeroize"]
|
||||
|
||||
[dependencies.picky-asn1-der]
|
||||
version = "0.5"
|
||||
|
||||
[dependencies.picky-asn1-x509]
|
||||
version = "0.15"
|
||||
features = [
|
||||
"legacy",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[dependencies.pkcs1]
|
||||
version = "^0.8.0-rc.4"
|
||||
|
||||
[dependencies.primeorder]
|
||||
version = "^0.14.0-rc.14"
|
||||
|
||||
[dependencies.rand]
|
||||
version = "0.10"
|
||||
|
||||
[dependencies.rand_core]
|
||||
version = "0.10"
|
||||
|
||||
[dependencies.rc2]
|
||||
version = "0.9"
|
||||
optional = true
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.13"
|
||||
features = ["blocking"]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.rsa]
|
||||
version = "^0.10.0-rc.18"
|
||||
features = ["std"]
|
||||
|
||||
[dependencies.rustcrypto-ff]
|
||||
version = "^0.14.0-rc.1"
|
||||
|
||||
[dependencies.rustcrypto-ff_derive]
|
||||
version = "^0.14.0-rc.0"
|
||||
|
||||
[dependencies.rustcrypto-group]
|
||||
version = "^0.14.0-rc.1"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.serde_json]
|
||||
version = "1"
|
||||
optional = true
|
||||
|
||||
[dependencies.sha1]
|
||||
version = "0.11"
|
||||
features = ["oid"]
|
||||
|
||||
[dependencies.sha2]
|
||||
version = "0.11"
|
||||
features = ["oid"]
|
||||
|
||||
[dependencies.sha3]
|
||||
version = "0.12"
|
||||
features = ["oid"]
|
||||
|
||||
[dependencies.thiserror]
|
||||
version = "2"
|
||||
|
||||
[dependencies.time]
|
||||
version = "0.3"
|
||||
optional = true
|
||||
|
||||
[dependencies.x25519-dalek]
|
||||
version = "^3.0.0-rc.1"
|
||||
features = ["static_secrets"]
|
||||
|
||||
[dependencies.zeroize]
|
||||
version = "1.8"
|
||||
|
||||
[dev-dependencies.cfg-if]
|
||||
version = "1.0"
|
||||
|
||||
[dev-dependencies.expect-test]
|
||||
version = "1"
|
||||
|
||||
[dev-dependencies.picky-test-data]
|
||||
version = "0.1"
|
||||
|
||||
[dev-dependencies.pretty_assertions]
|
||||
version = "1.4"
|
||||
|
||||
[dev-dependencies.rand_chacha]
|
||||
version = "0.10"
|
||||
|
||||
[dev-dependencies.ring]
|
||||
version = "0.17"
|
||||
|
||||
[dev-dependencies.rstest]
|
||||
version = "0.26"
|
||||
|
||||
[dev-dependencies.tempfile]
|
||||
version = "3.22"
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
[package]
|
||||
name = "picky"
|
||||
version = "7.0.0-rc.25"
|
||||
authors = [
|
||||
"Benoît CORTIER <bcortier@devolutions.net>",
|
||||
"Jonathan Trepanier <jtrepanier@devolutions.net>",
|
||||
"François Dubois <fdubois@devolutions.net>",
|
||||
"Richard Markiewicz <rmarkiewicz@devolutions.net>",
|
||||
"Ionut Mihalcea <ionut.mihalcea@arm.com>",
|
||||
"Kim Altintop <kim@monadic.xyz>",
|
||||
"Alexandr Yusuk <aleksandr.yusuk@apriorit.com>",
|
||||
"Flavio Castelli <flavio@castelli.me>",
|
||||
"Xynnn_ <xynnn@linux.alibaba.com>",
|
||||
]
|
||||
description = "Portable X.509, PKI, JOSE and HTTP signature implementation."
|
||||
keywords = ["x509", "jwt", "signature", "jose", "pki"]
|
||||
edition = "2024"
|
||||
rust-version = "1.85"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/Devolutions/picky-rs"
|
||||
include = ["src/**/*", "README.md", "CHANGELOG.md", "LICENSE-*"]
|
||||
|
||||
[dependencies]
|
||||
picky-asn1 = { version = "0.10", path = "../picky-asn1", features = ["zeroize"] }
|
||||
picky-asn1-der = { version = "0.5", path = "../picky-asn1-der" }
|
||||
picky-asn1-x509 = { version = "0.15", path = "../picky-asn1-x509", features = ["legacy", "zeroize"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
base64 = "0.22"
|
||||
thiserror = "2"
|
||||
byteorder = { version = "1.5", optional = true }
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock"], optional = true }
|
||||
time = { version = "0.3", optional = true }
|
||||
serde_json = { version = "1", optional = true }
|
||||
hex = "0.4"
|
||||
http = { version = "1.4", optional = true }
|
||||
cab = { version = "0.6", optional = true }
|
||||
lexical-sort = { version = "0.3", optional = true }
|
||||
zeroize = "1.8"
|
||||
|
||||
# FIXME: either use ureq, or even better: do not require this kind of dependency at all to let user decide which lib to use.
|
||||
# (currently users should *really* not forget to use `spawn_blocking` when calling associated functions from async context)
|
||||
reqwest = { version = "0.13", default-features = false, features = ["blocking"], optional = true }
|
||||
|
||||
# /!\ ===== cryptography dependencies ===== /!\
|
||||
# These should be updated as soon as possible.
|
||||
# /!\ ===================================== /!\
|
||||
|
||||
rand = "0.10"
|
||||
rand_core = "0.10"
|
||||
crypto-bigint = "0.7"
|
||||
|
||||
ed25519-dalek = { version = "=3.0.0-rc.1", features = ["hazmat", "rand_core"] }
|
||||
x25519-dalek = { version = "=3.0.0-rc.1", features = ["static_secrets"] }
|
||||
|
||||
p256 = { version = "=0.14.0-rc.14", features = ["ecdh"] }
|
||||
p384 = { version = "=0.14.0-rc.14", features = ["ecdh"] }
|
||||
p521 = { version = "=0.14.0-rc.14", features = ["ecdh"] }
|
||||
|
||||
rsa = { version = "=0.10.0-rc.18", features = ["std"] }
|
||||
|
||||
digest = "0.11"
|
||||
md5 = { package = "md-5", version = "0.11", features = ["oid"] }
|
||||
sha1 = { version = "0.11", features = ["oid"] }
|
||||
sha2 = { version = "0.11", features = ["oid"] }
|
||||
sha3 = { version = "0.12", features = ["oid"] }
|
||||
|
||||
aes-gcm = { version = "=0.11.0-rc.4", optional = true }
|
||||
aes = { version = "0.9", optional = true }
|
||||
aes-kw = { version = "0.3", optional = true }
|
||||
argon2 = { version = "=0.6.0-rc.8", optional = true }
|
||||
ctr = { version = "0.10", optional = true }
|
||||
cbc = { version = "0.2", optional = true, features = ["alloc"] }
|
||||
bcrypt-pbkdf = { version = "0.11", optional = true }
|
||||
des = { version = "0.9", optional = true }
|
||||
rc2 = { version = "0.9", optional = true }
|
||||
pbkdf2 = { version = "0.13", optional = true }
|
||||
hmac = { version = "0.13", optional = true }
|
||||
crypto-common = "0.2"
|
||||
inout = "0.2.2"
|
||||
|
||||
# Pin transitive dependencies versions.
|
||||
# TODO: Remove when stable versions will be released.
|
||||
blake2 = { version = "=0.11.0-rc.6", optional = true }
|
||||
ecdsa = "=0.17.0-rc.22"
|
||||
pkcs1 = "=0.8.0-rc.4"
|
||||
primeorder = "=0.14.0-rc.14"
|
||||
rustcrypto-ff = "=0.14.0-rc.1"
|
||||
rustcrypto-ff_derive = "=0.14.0-rc.0"
|
||||
rustcrypto-group = "=0.14.0-rc.1"
|
||||
curve25519-dalek = "=5.0.0-rc.1"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4"
|
||||
cfg-if = "1.0"
|
||||
rand_chacha = "0.10"
|
||||
ring = "0.17"
|
||||
rstest = "0.26"
|
||||
expect-test = "1"
|
||||
tempfile = "3.22"
|
||||
picky-test-data = { path = "../picky-test-data", version = "0.1" }
|
||||
|
||||
[features]
|
||||
default = ["x509", "jose", "http_signature", "http_trait_impl", "pkcs12"]
|
||||
|
||||
# main features
|
||||
x509 = []
|
||||
jose = ["dep:serde_json", "dep:aes-gcm", "dep:ctr", "dep:cbc", "dep:aes-kw", "dep:aes"]
|
||||
http_signature = []
|
||||
pkcs12 = ["picky-asn1-x509/pkcs12", "dep:des", "dep:rc2", "dep:cbc", "dep:hmac", "dep:aes", "x509", "dep:pbkdf2"]
|
||||
|
||||
# secondary features
|
||||
pkcs7 = ["x509", "picky-asn1-x509/pkcs7"]
|
||||
http_timestamp = ["dep:reqwest"]
|
||||
ctl = ["picky-asn1-x509/ctl", "pkcs7", "chrono_conversion"]
|
||||
ctl_http_fetch = ["dep:reqwest", "dep:cab", "ctl"]
|
||||
wincert = ["x509", "dep:byteorder"]
|
||||
ssh = ["dep:byteorder", "dep:aes", "dep:ctr", "dep:cbc", "dep:bcrypt-pbkdf", "dep:lexical-sort", "dep:hmac", "dep:pbkdf2"]
|
||||
http_trait_impl = ["dep:http"]
|
||||
chrono_conversion = ["dep:chrono", "picky-asn1/chrono_conversion"]
|
||||
time_conversion = ["dep:time", "picky-asn1/time_conversion"]
|
||||
putty = ["dep:argon2", "dep:hmac", "ssh",
|
||||
# Transitive dependencies
|
||||
"dep:blake2"
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
# Enable all features when building documentation for docs.rs
|
||||
all-features = true
|
||||
Vendored
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
[](https://crates.io/crates/picky)
|
||||
[](https://docs.rs/picky)
|
||||

|
||||
|
||||
Compatible with rustc 1.85.
|
||||
Minimal rustc version bumps happen [only with minor number bumps in this project](https://github.com/Devolutions/picky-rs/issues/89#issuecomment-868303478).
|
||||
|
||||
# picky
|
||||
|
||||
Portable X.509, PKI, JOSE and HTTP signature implementation.
|
||||
|
||||
## X.509 / PKI
|
||||
|
||||
[See doc](https://docs.rs/picky/latest/picky/x509/index.html) for tested examples.
|
||||
|
||||
## HTTP signature
|
||||
|
||||
[See doc](https://docs.rs/picky/latest/picky/http/index.html) for tested examples.
|
||||
|
||||
## JOSE
|
||||
|
||||
Doc doesn't have example yet, but [tests](https://github.com/Devolutions/picky-rs/blob/master/picky/src/jose/jwt.rs#L438) are good reference.
|
||||
Vendored
+101
@@ -0,0 +1,101 @@
|
||||
//! Hash algorithms supported by picky
|
||||
|
||||
use digest::Digest;
|
||||
use picky_asn1_x509::ShaVariant;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
/// unsupported algorithm
|
||||
#[derive(Debug)]
|
||||
pub struct UnsupportedHashAlgorithmError {
|
||||
pub algorithm: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for UnsupportedHashAlgorithmError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "unsupported algorithm: {}", self.algorithm)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for UnsupportedHashAlgorithmError {}
|
||||
|
||||
/// Supported hash algorithms
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum HashAlgorithm {
|
||||
MD5,
|
||||
SHA1,
|
||||
SHA2_224,
|
||||
SHA2_256,
|
||||
SHA2_384,
|
||||
SHA2_512,
|
||||
SHA3_384,
|
||||
SHA3_512,
|
||||
}
|
||||
|
||||
impl TryFrom<HashAlgorithm> for ShaVariant {
|
||||
type Error = UnsupportedHashAlgorithmError;
|
||||
|
||||
fn try_from(v: HashAlgorithm) -> Result<ShaVariant, UnsupportedHashAlgorithmError> {
|
||||
match v {
|
||||
HashAlgorithm::MD5 => Ok(ShaVariant::MD5),
|
||||
HashAlgorithm::SHA1 => Ok(ShaVariant::SHA1),
|
||||
HashAlgorithm::SHA2_256 => Ok(ShaVariant::SHA2_256),
|
||||
HashAlgorithm::SHA2_384 => Ok(ShaVariant::SHA2_384),
|
||||
HashAlgorithm::SHA2_512 => Ok(ShaVariant::SHA2_512),
|
||||
HashAlgorithm::SHA3_384 => Ok(ShaVariant::SHA3_384),
|
||||
HashAlgorithm::SHA3_512 => Ok(ShaVariant::SHA3_512),
|
||||
_ => Err(UnsupportedHashAlgorithmError {
|
||||
algorithm: format!("{v:?}"),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ShaVariant> for HashAlgorithm {
|
||||
type Error = UnsupportedHashAlgorithmError;
|
||||
|
||||
fn try_from(v: ShaVariant) -> Result<HashAlgorithm, UnsupportedHashAlgorithmError> {
|
||||
match v {
|
||||
ShaVariant::MD5 => Ok(HashAlgorithm::MD5),
|
||||
ShaVariant::SHA1 => Ok(HashAlgorithm::SHA1),
|
||||
ShaVariant::SHA2_256 => Ok(HashAlgorithm::SHA2_256),
|
||||
ShaVariant::SHA2_384 => Ok(HashAlgorithm::SHA2_384),
|
||||
ShaVariant::SHA2_512 => Ok(HashAlgorithm::SHA2_512),
|
||||
ShaVariant::SHA3_384 => Ok(HashAlgorithm::SHA3_384),
|
||||
ShaVariant::SHA3_512 => Ok(HashAlgorithm::SHA3_512),
|
||||
_ => Err(UnsupportedHashAlgorithmError {
|
||||
algorithm: format!("{v:?}"),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HashAlgorithm {
|
||||
pub fn digest(self, msg: &[u8]) -> Vec<u8> {
|
||||
match self {
|
||||
Self::MD5 => md5::Md5::digest(msg).as_slice().to_vec(),
|
||||
Self::SHA1 => sha1::Sha1::digest(msg).as_slice().to_vec(),
|
||||
Self::SHA2_224 => sha2::Sha224::digest(msg).as_slice().to_vec(),
|
||||
Self::SHA2_256 => sha2::Sha256::digest(msg).as_slice().to_vec(),
|
||||
Self::SHA2_384 => sha2::Sha384::digest(msg).as_slice().to_vec(),
|
||||
Self::SHA2_512 => sha2::Sha512::digest(msg).as_slice().to_vec(),
|
||||
Self::SHA3_384 => sha3::Sha3_384::digest(msg).as_slice().to_vec(),
|
||||
Self::SHA3_512 => sha3::Sha3_512::digest(msg).as_slice().to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_size(self) -> usize {
|
||||
match self {
|
||||
Self::MD5 => md5::Md5::output_size(),
|
||||
Self::SHA1 => sha1::Sha1::output_size(),
|
||||
Self::SHA2_224 => sha2::Sha224::output_size(),
|
||||
Self::SHA2_256 => sha2::Sha256::output_size(),
|
||||
Self::SHA2_384 => sha2::Sha384::output_size(),
|
||||
Self::SHA2_512 => sha2::Sha512::output_size(),
|
||||
Self::SHA3_384 => sha3::Sha3_384::output_size(),
|
||||
Self::SHA3_512 => sha3::Sha3_512::output_size(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
use std::borrow::Cow;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum HttpRequestError {
|
||||
/// couldn't convert a http header value to string
|
||||
#[error("couldn't convert http header value to string for header key {key}")]
|
||||
HeaderValueToStr { key: String },
|
||||
|
||||
/// unexpected error occurred
|
||||
#[error("unexpected error: {reason}")]
|
||||
Unexpected { reason: String },
|
||||
}
|
||||
|
||||
pub trait HttpRequest {
|
||||
fn get_header_concatenated_values<'a>(&'a self, header_name: &str) -> Result<Cow<'a, str>, HttpRequestError>;
|
||||
fn get_lowercased_method(&self) -> Result<Cow<'_, str>, HttpRequestError>;
|
||||
fn get_target(&self) -> Result<Cow<'_, str>, HttpRequestError>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "http_trait_impl")]
|
||||
mod http_trait_impl {
|
||||
use super::*;
|
||||
|
||||
impl HttpRequest for http::request::Parts {
|
||||
fn get_header_concatenated_values<'a>(&'a self, header_name: &str) -> Result<Cow<'a, str>, HttpRequestError> {
|
||||
let mut values = Vec::new();
|
||||
let all_values = self.headers.get_all(header_name);
|
||||
for value in all_values {
|
||||
let value_str = value.to_str().map_err(|_| HttpRequestError::HeaderValueToStr {
|
||||
key: header_name.to_owned(),
|
||||
})?;
|
||||
values.push(value_str.trim());
|
||||
}
|
||||
Ok(Cow::Owned(values.join(", ")))
|
||||
}
|
||||
|
||||
fn get_lowercased_method(&self) -> Result<Cow<'_, str>, HttpRequestError> {
|
||||
Ok(Cow::Owned(self.method.as_str().to_lowercase()))
|
||||
}
|
||||
|
||||
fn get_target(&self) -> Result<Cow<'_, str>, HttpRequestError> {
|
||||
Ok(Cow::Borrowed(self.uri.path()))
|
||||
}
|
||||
}
|
||||
impl<T> HttpRequest for http::request::Request<T> {
|
||||
fn get_header_concatenated_values<'a>(&'a self, header_name: &str) -> Result<Cow<'a, str>, HttpRequestError> {
|
||||
let mut values = Vec::new();
|
||||
let all_values = self.headers().get_all(header_name);
|
||||
for value in all_values {
|
||||
let value_str = value.to_str().map_err(|_| HttpRequestError::HeaderValueToStr {
|
||||
key: header_name.to_owned(),
|
||||
})?;
|
||||
values.push(value_str.trim());
|
||||
}
|
||||
Ok(Cow::Owned(values.join(", ")))
|
||||
}
|
||||
|
||||
fn get_lowercased_method(&self) -> Result<Cow<'_, str>, HttpRequestError> {
|
||||
Ok(Cow::Owned(self.method().as_str().to_lowercase()))
|
||||
}
|
||||
|
||||
fn get_target(&self) -> Result<Cow<'_, str>, HttpRequestError> {
|
||||
Ok(Cow::Borrowed(self.uri().path()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use http::method::Method;
|
||||
use http::{header, request};
|
||||
|
||||
#[test]
|
||||
fn http_request_parts() {
|
||||
let req = request::Builder::new()
|
||||
.method(Method::GET)
|
||||
.uri("/foo")
|
||||
.header("Host", "example.org")
|
||||
.header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT")
|
||||
.header("X-Example", " Example header with some whitespace. ")
|
||||
.header("X-EmptyHeader", "")
|
||||
.header(header::CACHE_CONTROL, "max-age=60")
|
||||
.header(header::CACHE_CONTROL, "must-revalidate")
|
||||
.body(())
|
||||
.expect("couldn't build request");
|
||||
|
||||
let (parts, _) = req.into_parts();
|
||||
|
||||
assert_eq!(parts.get_target().expect("target"), "/foo");
|
||||
assert_eq!(parts.get_lowercased_method().expect("method"), "get");
|
||||
assert_eq!(
|
||||
parts.get_header_concatenated_values("host").expect("host"),
|
||||
"example.org"
|
||||
);
|
||||
assert_eq!(
|
||||
parts.get_header_concatenated_values("date").expect("date"),
|
||||
"Tue, 07 Jun 2014 20:51:35 GMT"
|
||||
);
|
||||
assert_eq!(
|
||||
parts.get_header_concatenated_values("x-example").expect("example"),
|
||||
"Example header with some whitespace."
|
||||
);
|
||||
assert_eq!(
|
||||
parts.get_header_concatenated_values("X-EmptyHeader").expect("empty"),
|
||||
""
|
||||
);
|
||||
assert_eq!(
|
||||
parts
|
||||
.get_header_concatenated_values(header::CACHE_CONTROL.as_str())
|
||||
.expect("cache control"),
|
||||
"max-age=60, must-revalidate"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_request_request() {
|
||||
let req = request::Builder::new()
|
||||
.method(Method::GET)
|
||||
.uri("/foo")
|
||||
.header("Host", "example.org")
|
||||
.header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT")
|
||||
.header("X-Example", " Example header with some whitespace. ")
|
||||
.header("X-EmptyHeader", "")
|
||||
.header(header::CACHE_CONTROL, "max-age=60")
|
||||
.header(header::CACHE_CONTROL, "must-revalidate")
|
||||
.body(())
|
||||
.expect("couldn't build request");
|
||||
|
||||
assert_eq!(req.get_target().expect("target"), "/foo");
|
||||
assert_eq!(req.get_lowercased_method().expect("method"), "get");
|
||||
assert_eq!(req.get_header_concatenated_values("host").expect("host"), "example.org");
|
||||
assert_eq!(
|
||||
req.get_header_concatenated_values("date").expect("date"),
|
||||
"Tue, 07 Jun 2014 20:51:35 GMT"
|
||||
);
|
||||
assert_eq!(
|
||||
req.get_header_concatenated_values("x-example").expect("example"),
|
||||
"Example header with some whitespace."
|
||||
);
|
||||
assert_eq!(req.get_header_concatenated_values("X-EmptyHeader").expect("empty"), "");
|
||||
assert_eq!(
|
||||
req.get_header_concatenated_values(header::CACHE_CONTROL.as_str())
|
||||
.expect("cache control"),
|
||||
"max-age=60, must-revalidate"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1313
File diff suppressed because it is too large
Load Diff
Vendored
+143
@@ -0,0 +1,143 @@
|
||||
//! Signing HTTP Messages
|
||||
//!
|
||||
//! This module provides an implementation of a subset of
|
||||
//! [draft-cavage-http-signatures-12 RFC](https://tools.ietf.org/html/draft-cavage-http-signatures-12).
|
||||
//!
|
||||
//! # Example
|
||||
//! ```
|
||||
//! use picky::{
|
||||
//! http::http_signature::{HttpSignatureBuilder, HttpSignature},
|
||||
//! signature::SignatureAlgorithm,
|
||||
//! hash::HashAlgorithm,
|
||||
//! key::PrivateKey,
|
||||
//! pem::parse_pem,
|
||||
//! };
|
||||
//! use http::{request, header::{self, HeaderName}, method::Method};
|
||||
//!
|
||||
//! // all you need to generate a http signature
|
||||
//!
|
||||
//! let private_rsa_key: &str = "-----BEGIN RSA PRIVATE KEY-----\n\
|
||||
//! MIIEpgIBAAKCAQEApDx0MjvRzmxYXKfqHy0gN1znX6rSU2EnsDTbZaU1UcsMmRNx\n\
|
||||
//! L+FqDNzwNutCSQlkujzR37+bHGTxOnRvvSG3lwRvDBZepYWPum9WDqa9T5gTS/Cj\n\
|
||||
//! luq/oSsOyt/tUDO/GcNPTTfUQlgtOZ+zRo6FA0rpAQ8CrQm7XzGQ0DMoDU1SVNnu\n\
|
||||
//! tFJowlece9Y4NtAfhA+kJ5IEmcE9AgwxJY/iCyCxUEBUe7biwbUafLdtA3+3S8Bu\n\
|
||||
//! hBXAr+1BING3qS0vl08+3eaFq5q7f7VwcYOhUmH13itqSGwDznCk4oDQl+qn9DQZ\n\
|
||||
//! X9/09KtsgxuIcozxj0RwGKX8qkz4TlAGJw+oNwIDAQABAoIBAQCN9IrimH3iFBfU\n\
|
||||
//! Dnb4d4KvF6gNMpMU6pbpYOZ51vBdQEolTX65yfZmI9mlPndOtcXQi51D7lNdmYo/\n\
|
||||
//! 4kBqk2giKfzpz7QDEYyHspAJnelnkKStMNPVMBZucc8ZX6+5cOCunfg/YBAhQCHm\n\
|
||||
//! +rh0Nd+WVvtKpPTFJ/JCd48Zxf3KcDZD+AsWTjPt4zte8KdcwxiD3MrFunxgeujX\n\
|
||||
//! n0U0/f7hvX/7JBQ20gu2tD9whEaS2Gn8E4WpEV8wC6Ah1pU9mZNZ0u8clW9SV0de\n\
|
||||
//! ay0mHw8y/Wx6rkEMvrecK6mWbwSQGfRq+crI9PCwA5wn/EZmpQrQs9r5MLtDKVsQ\n\
|
||||
//! r9axQrSRAoGBANB53u3mxY9ByiYGT5Ge/33+BjANXxmIPG0TWgV1D8MhwFiaiHF+\n\
|
||||
//! tiEzoz4vi23Q+GeHyoM1wxw8VurDX+vcIJbZ0dyGOM/6F0eago7ZAtvHMdUdahAO\n\
|
||||
//! X+klqG8kIysgFXSzaU2w76816iIaXiZlDZUghrnd3wmgu9jhl3HCUlltAoGBAMms\n\
|
||||
//! 2uufuk26nssF+woQuy017lgLUNFCRrO9F3iwIiyY5R/q372gsx8HVzjYGKY8CF6v\n\
|
||||
//! m6JFfxogp44ZcafYOeu+iXbqoCAK4BTdbFB7/D3rX7WgidaxUlLGoXsNFIUIVubR\n\
|
||||
//! jaRA7l3tl3fkpdqUAye6zosMKp2oybQLyX5hLAWzAoGBAJKhVUIA8W1cOaFbCPYE\n\
|
||||
//! XfExDQsZLI1ZvB5/4O47srVtdMsdDeC93b4mgqfHawr3UvAGm1KEKtIeQofmmP3c\n\
|
||||
//! mvNfCvNPWIA3h84uB6wPSKpqRUt+382hPqZOfVSGl1HKxCyL0AH78+lJQ39vCk94\n\
|
||||
//! /f+om/n46tnrupPFv+4cXi1VAoGBALzSSmYxtozwHZyYjOJvp9A8nltwvMov82J1\n\
|
||||
//! uHQW9OgsftnTXoh83Tg/9zoRmYKK0otUf7L+vnIIANjamb88g35ldu8P3bwicosW\n\
|
||||
//! hUMV0qVmqsWy+Vs5yooVzzsWlA+6LyMNMECJSqRGv3pRabesvQeFr7wgOAZE8hTQ\n\
|
||||
//! tGbPNBhhAoGBAIkXxIJT0OMKSt/A7wDE9wd3dtC8mbkqr5aZTvwiuD6OvNDdXb/J\n\
|
||||
//! i03ns56mIflifVLPYVmCEXdYIzSv7HfeR4d78bAvqiMFfnQ2PF3tuoKMSvQM0m8/\n\
|
||||
//! f3VhEFFMrUTTRMX/9PR0ITQtnZlWIDfBVgXPmWqTqCGOMYsRPv70LGse\n\
|
||||
//! -----END RSA PRIVATE KEY-----";
|
||||
//! let pem = parse_pem(private_rsa_key).expect("couldn't parse pem");
|
||||
//! let private_key = PrivateKey::from_pem(&pem).expect("couldn't parse private key");
|
||||
//!
|
||||
//! let req = request::Builder::new()
|
||||
//! .method(Method::GET)
|
||||
//! .uri("/foo")
|
||||
//! .header("Host", "example.org")
|
||||
//! .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT")
|
||||
//! .header("X-Example", " Example header with some whitespace. ")
|
||||
//! .header("X-EmptyHeader", "")
|
||||
//! .header(header::CACHE_CONTROL, "max-age=60")
|
||||
//! .header(header::CACHE_CONTROL, "must-revalidate")
|
||||
//! .body(())
|
||||
//! .expect("couldn't build request");
|
||||
//!
|
||||
//! let (parts, _) = req.into_parts();
|
||||
//!
|
||||
//! // generate http signature
|
||||
//!
|
||||
//! let http_signature = HttpSignatureBuilder::new()
|
||||
//! .key_id("my-rsa-key")
|
||||
//! .signature_method(&private_key, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224))
|
||||
//! // `picky::http::http_request::HttpRequest` trait is implemented for `http::request::Parts`
|
||||
//! // for `http` crate with `http_trait_impl` feature gate
|
||||
//! .generate_signing_string_using_http_request(&parts)
|
||||
//! .request_target()
|
||||
//! .created(1402170695)
|
||||
//! .http_header("host")
|
||||
//! .http_header(header::DATE.as_str())
|
||||
//! .http_header(header::CACHE_CONTROL.as_str())
|
||||
//! .http_header("x-emptyheader")
|
||||
//! .http_header("X-EXAMPLE")
|
||||
//! .build()
|
||||
//! .expect("couldn't generate http signature");
|
||||
//!
|
||||
//! let http_signature_str = http_signature.to_string();
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! http_signature_str,
|
||||
//! "Signature keyId=\"my-rsa-key\",algorithm=\"rsa-sha224\",created=1402170695,\
|
||||
//! headers=\"(request-target) (created) host date cache-control x-emptyheader x-example\",\
|
||||
//! signature=\"JueyecQbV5rQ3TI1EfqZRjAZMMOb4ABZNS0yDcBDgyfbOLORYipT2An2MCH8n/HequVJkEE\
|
||||
//! 86/vj9ZFLbyqFkV3a8uQGB6gaE79l9YNdzVeO5k7GBb1jskwBXnqVtGmn8aT2f+cJzkDtu6ptg+UtaU\
|
||||
//! ZOQKdutc8aHq1NCLwvqMbA410XP0pA5r/VTbMg/yW8rHguue0Trh0WYCw8zHfNuZtheWxvWGLdYxvC0\
|
||||
//! u5oJA0PdxFceqVd/304+RQsrDGLtX8J9vSeqEsQJfvswFyMTkdl1gDbP/YdXp7ADzc2D9IefT9zqvFd\
|
||||
//! yEDDXEXKmZm+22395xRtnFmeWXu/+PM6wg==\""
|
||||
//! );
|
||||
//!
|
||||
//! // parse a http signature and verify it
|
||||
//!
|
||||
//! let parsed_http_signature = http_signature_str.parse::<HttpSignature>()
|
||||
//! .expect("couldn't parse http signature");
|
||||
//!
|
||||
//! assert_eq!(parsed_http_signature, http_signature);
|
||||
//!
|
||||
//! parsed_http_signature.verifier()
|
||||
//! .signature_method(&private_key.to_public_key().unwrap(), SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224))
|
||||
//! .generate_signing_string_using_http_request(&parts)
|
||||
//! .now(1402170695)
|
||||
//! .verify()
|
||||
//! .expect("couldn't verify signature");
|
||||
//!
|
||||
//! // alternatively you can provide a pre-generated signing string
|
||||
//!
|
||||
//! let signing_string =
|
||||
//! "(request-target): get /foo\n\
|
||||
//! (created): 1402170695\n\
|
||||
//! host: example.org\n\
|
||||
//! date: Tue, 07 Jun 2014 20:51:35 GMT\n\
|
||||
//! cache-control: max-age=60, must-revalidate\n\
|
||||
//! x-emptyheader:\n\
|
||||
//! x-example: Example header with some whitespace.";
|
||||
//!
|
||||
//! let http_signature_pre_generated = HttpSignatureBuilder::new()
|
||||
//! .key_id("my-rsa-key")
|
||||
//! .signature_method(&private_key, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224))
|
||||
//! .pre_generated_signing_string(signing_string)
|
||||
//! .build()
|
||||
//! .expect("couldn't generate http signature using pre-generated signing string");
|
||||
//!
|
||||
//! let http_signature_pre_generated_str = http_signature_pre_generated.to_string();
|
||||
//!
|
||||
//! assert_eq!(http_signature_pre_generated, http_signature);
|
||||
//! assert_eq!(http_signature_pre_generated_str, http_signature_str);
|
||||
//!
|
||||
//! parsed_http_signature.verifier()
|
||||
//! .signature_method(&private_key.to_public_key().unwrap(), SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224))
|
||||
//! .pre_generated_signing_string(signing_string)
|
||||
//! .now(1402170695)
|
||||
//! .verify()
|
||||
//! .expect("couldn't verify signature using pre-generated signing string");
|
||||
//! ```
|
||||
|
||||
pub mod http_request;
|
||||
pub mod http_signature;
|
||||
|
||||
pub use http_request::HttpRequest;
|
||||
pub use http_signature::HttpSignature;
|
||||
Vendored
+1602
File diff suppressed because it is too large
Load Diff
Vendored
+767
@@ -0,0 +1,767 @@
|
||||
//! A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data structure that represents a cryptographic key.
|
||||
//!
|
||||
//! See [RFC7517](https://tools.ietf.org/html/rfc7517).
|
||||
|
||||
use crate::jose::jwe::{JweAlg, JweEnc};
|
||||
use crate::jose::jws::JwsAlg;
|
||||
use crate::key::ec::{EcdsaPublicKey, NamedEcCurve};
|
||||
use crate::key::ed::{EdPublicKey, NamedEdAlgorithm};
|
||||
use crate::key::{EcCurve, EdAlgorithm, PublicKey};
|
||||
use base64::engine::general_purpose;
|
||||
use base64::{DecodeError, Engine as _};
|
||||
use crypto_bigint::BoxedUint;
|
||||
use picky_asn1::wrapper::IntegerAsn1;
|
||||
use picky_asn1_x509::SubjectPublicKeyInfo;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
// === error type === //
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum JwkError {
|
||||
/// Json error
|
||||
#[error("JSON error: {source}")]
|
||||
Json { source: serde_json::Error },
|
||||
|
||||
/// couldn't decode base64
|
||||
#[error("couldn't decode base64: {source}")]
|
||||
Base64Decoding { source: DecodeError },
|
||||
|
||||
/// unsupported algorithm
|
||||
#[error("unsupported algorithm: {algorithm}")]
|
||||
UnsupportedAlgorithm { algorithm: &'static str },
|
||||
|
||||
#[error("invalid ec public key: {cause}")]
|
||||
InvalidEcPublicKey { cause: String },
|
||||
|
||||
#[error("invalid ec point coordinates in JWK")]
|
||||
InvalidEcPointCoordinates,
|
||||
|
||||
#[error("invalid ed public key: {cause}")]
|
||||
InvalidEdPublicKey { cause: String },
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for JwkError {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Self::Json { source: e }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DecodeError> for JwkError {
|
||||
fn from(e: DecodeError) -> Self {
|
||||
Self::Base64Decoding { source: e }
|
||||
}
|
||||
}
|
||||
|
||||
// === key type === //
|
||||
|
||||
/// Algorithm type for JWK
|
||||
///
|
||||
/// See [RFC7518 #6](https://tools.ietf.org/html/rfc7518#section-6.1)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(tag = "kty")]
|
||||
pub enum JwkKeyType {
|
||||
/// Edwards curve-based cryptography
|
||||
///
|
||||
/// Defined by separate [RFC 8037](https://www.rfc-editor.org/rfc/rfc8037)
|
||||
#[serde(rename = "OKP")]
|
||||
Ed(JwkPublicEdKey),
|
||||
/// Elliptic Curve
|
||||
///
|
||||
/// Recommended+ by RFC
|
||||
#[serde(rename = "EC")]
|
||||
Ec(JwkPublicEcKey),
|
||||
/// Elliptic Curve
|
||||
///
|
||||
/// Required by RFC
|
||||
#[serde(rename = "RSA")]
|
||||
Rsa(JwkPublicRsaKey),
|
||||
/// Octet sequence (used to represent symmetric keys) (unsupported)
|
||||
///
|
||||
/// Required by RFC
|
||||
#[serde(rename = "oct")]
|
||||
Oct,
|
||||
}
|
||||
|
||||
impl JwkKeyType {
|
||||
/// Build a JWK key from RSA components.
|
||||
///
|
||||
/// Each argument is the unsigned big-endian representation as an octet sequence of the value.
|
||||
/// If a signed representation is provided, leading zero is removed for any number bigger than 0x7F.
|
||||
pub fn new_rsa_key(modulus: &[u8], public_exponent: &[u8]) -> Self {
|
||||
let modulus = h_strip_unrequired_leading_zero(modulus);
|
||||
let public_exponent = h_strip_unrequired_leading_zero(public_exponent);
|
||||
Self::Rsa(JwkPublicRsaKey {
|
||||
n: general_purpose::URL_SAFE_NO_PAD.encode(modulus),
|
||||
e: general_purpose::URL_SAFE_NO_PAD.encode(public_exponent),
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a JWK key from EC components.
|
||||
///
|
||||
/// `x` and `y` are big-endian representation of the affine point coordinates.
|
||||
pub fn new_ec_key(curve: JwkEcPublicKeyCurve, x: &[u8], y: &[u8]) -> Self {
|
||||
let x = h_strip_unrequired_leading_zero(x);
|
||||
let y = h_strip_unrequired_leading_zero(y);
|
||||
Self::Ec(JwkPublicEcKey {
|
||||
crv: curve,
|
||||
x: general_purpose::URL_SAFE_NO_PAD.encode(x),
|
||||
y: general_purpose::URL_SAFE_NO_PAD.encode(y),
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a JWK key from Edwards curve components.
|
||||
///
|
||||
/// `crv` is ed-based algorithm name.
|
||||
/// `x` is raw public key bytes.
|
||||
pub fn new_ed_key(crv: JwkEdPublicKeyAlgorithm, x: &[u8]) -> Self {
|
||||
Self::Ed(JwkPublicEdKey {
|
||||
crv,
|
||||
x: general_purpose::URL_SAFE_NO_PAD.encode(x),
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a JWK key from RSA components already encoded following base64 url format.
|
||||
///
|
||||
/// Each argument is the unsigned big-endian representation as an octet sequence of the value.
|
||||
/// The octet sequence MUST utilize the minimum number of octets needed to represent the value.
|
||||
/// That is: **no leading zero** must be present.
|
||||
///
|
||||
/// See definition for term `Base64urlUInt` in [RFC7518 section 2](https://datatracker.ietf.org/doc/html/rfc7518#section-2)
|
||||
pub fn new_rsa_key_from_base64_url(modulus: String, public_exponent: String) -> Self {
|
||||
Self::Rsa(JwkPublicRsaKey {
|
||||
n: modulus,
|
||||
e: public_exponent,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn as_rsa(&self) -> Option<&JwkPublicRsaKey> {
|
||||
match self {
|
||||
JwkKeyType::Rsa(rsa) => Some(rsa),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_ec(&self) -> Option<&JwkPublicEcKey> {
|
||||
match self {
|
||||
JwkKeyType::Ec(ec) => Some(ec),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_ed(&self) -> Option<&JwkPublicEdKey> {
|
||||
match self {
|
||||
JwkKeyType::Ed(ed) => Some(ed),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_rsa(&self) -> bool {
|
||||
self.as_rsa().is_some()
|
||||
}
|
||||
|
||||
pub fn is_ec(&self) -> bool {
|
||||
self.as_ec().is_some()
|
||||
}
|
||||
|
||||
pub fn is_ed(&self) -> bool {
|
||||
self.as_ed().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// Strips leading zero for any number bigger than 0x7F.
|
||||
fn h_strip_unrequired_leading_zero(value: &[u8]) -> &[u8] {
|
||||
if let [0x00, rest @ ..] = value { rest } else { value }
|
||||
}
|
||||
|
||||
/// Big integers from 0x00 to 0x7F are all base64-encoded using two ASCII characters ranging from "AA" to "fw".
|
||||
/// We know the required capacity is _exactly_ of one byte.
|
||||
/// The value 0 is valid and is represented as the array [0x00] ("AA").
|
||||
/// For numbers greater than 0x7F, logic is a bit more complex.
|
||||
/// There is no leading zero in JWK keys because _unsigned_ numbers are used.
|
||||
/// As such, there is no need to disambiguate the high-order bit (0x80)
|
||||
/// which is used as the sign bit for _signed_ numbers.
|
||||
/// The high-order bit is set when base64 encoding's leading character matches [g-z0-9_-].
|
||||
fn h_allocate_signed_big_int_buffer(base64_url_encoding: &str) -> Vec<u8> {
|
||||
match base64_url_encoding.chars().next() {
|
||||
// The leading zero is re-introduced for any number whose high-order bit is set
|
||||
Some('g'..='z' | '0'..='9' | '_' | '-') => vec![0],
|
||||
// Otherwise, there is nothing more to do
|
||||
_ => Vec::with_capacity(1),
|
||||
}
|
||||
}
|
||||
|
||||
// === public key use === //
|
||||
|
||||
/// Public Key Use, identifies the intended use of the public key.
|
||||
///
|
||||
/// See [RFC7517 #4](https://tools.ietf.org/html/rfc7517#section-4.2)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum JwkPubKeyUse {
|
||||
#[serde(rename = "sig")]
|
||||
Signature,
|
||||
#[serde(rename = "enc")]
|
||||
Encryption,
|
||||
}
|
||||
|
||||
// === key operations === //
|
||||
|
||||
/// Key Operations, identifies the operation(s) for which the key is intended to be used.
|
||||
///
|
||||
/// See [RFC7517 #4](https://tools.ietf.org/html/rfc7517#section-4.3)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum JwkKeyOps {
|
||||
#[serde(rename = "sign")]
|
||||
Sign,
|
||||
#[serde(rename = "verify")]
|
||||
Verify,
|
||||
#[serde(rename = "encrypt")]
|
||||
Encrypt,
|
||||
#[serde(rename = "decrypt")]
|
||||
Decrypt,
|
||||
#[serde(rename = "wrapKey")]
|
||||
WrapKey,
|
||||
#[serde(rename = "unwrapKey")]
|
||||
UnwrapKey,
|
||||
#[serde(rename = "deriveKey")]
|
||||
DeriveKey,
|
||||
#[serde(rename = "deriveBits")]
|
||||
DeriveBits,
|
||||
}
|
||||
|
||||
// === algorithms === //
|
||||
|
||||
/// JOSE algorithms names as defined by [RFC7518](https://tools.ietf.org/html/rfc7518)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Jwa {
|
||||
Sig(JwsAlg),
|
||||
Enc(JweEnc),
|
||||
CEKAlg(JweAlg),
|
||||
}
|
||||
|
||||
// === json web key === //
|
||||
|
||||
/// Represents a cryptographic key as defined by [RFC7517](https://tools.ietf.org/html/rfc7517).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct Jwk {
|
||||
// -- specific to JWK -- //
|
||||
#[serde(flatten)]
|
||||
pub key: JwkKeyType,
|
||||
|
||||
/// Identifies the algorithm intended for use with the key.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub alg: Option<Jwa>,
|
||||
|
||||
/// Public Key Use
|
||||
///
|
||||
/// Intended use of the public key.
|
||||
#[serde(rename = "use", skip_serializing_if = "Option::is_none")]
|
||||
pub key_use: Option<JwkPubKeyUse>,
|
||||
|
||||
/// Key Operations
|
||||
///
|
||||
/// identifies the operation(s) for which the key is intended to be used.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub key_ops: Option<Vec<JwkKeyOps>>,
|
||||
|
||||
// -- common with all -- //
|
||||
/// Key ID Header
|
||||
///
|
||||
/// A hint indicating which key was used.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub kid: Option<String>,
|
||||
|
||||
/// X.509 URL Header
|
||||
///
|
||||
/// URI that refers to a resource for an X.509 public key certificate or certificate chain.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub x5u: Option<String>,
|
||||
|
||||
/// X.509 Certificate Chain
|
||||
///
|
||||
/// Chain of one or more PKIX certificates.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub x5c: Option<Vec<String>>,
|
||||
|
||||
/// X.509 Certificate SHA-1 Thumbprint
|
||||
///
|
||||
/// base64url-encoded SHA-1 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub x5t: Option<String>,
|
||||
|
||||
/// X.509 Certificate SHA-256 Thumbprint
|
||||
///
|
||||
/// base64url-encoded SHA-256 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate.
|
||||
#[serde(rename = "x5t#S256", alias = "x5t#s256", skip_serializing_if = "Option::is_none")]
|
||||
pub x5t_s256: Option<String>,
|
||||
}
|
||||
|
||||
impl Jwk {
|
||||
pub fn new(key: JwkKeyType) -> Self {
|
||||
Jwk {
|
||||
key,
|
||||
alg: None,
|
||||
key_use: None,
|
||||
key_ops: None,
|
||||
kid: None,
|
||||
x5u: None,
|
||||
x5c: None,
|
||||
x5t: None,
|
||||
x5t_s256: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_json(json: &str) -> Result<Self, JwkError> {
|
||||
Ok(serde_json::from_str(json)?)
|
||||
}
|
||||
|
||||
pub fn from_public_key(public_key: &PublicKey) -> Result<Self, JwkError> {
|
||||
use picky_asn1::wrapper::BitStringAsn1Container;
|
||||
use picky_asn1_x509::PublicKey as SerdePublicKey;
|
||||
|
||||
match &public_key.as_inner().subject_public_key {
|
||||
SerdePublicKey::Rsa(BitStringAsn1Container(rsa)) => {
|
||||
let modulus = rsa.modulus.as_signed_bytes_be();
|
||||
let public_exponent = rsa.public_exponent.as_signed_bytes_be();
|
||||
Ok(Self::new(JwkKeyType::new_rsa_key(modulus, public_exponent)))
|
||||
}
|
||||
SerdePublicKey::Ec(_) => {
|
||||
let ec_key = EcdsaPublicKey::try_from(public_key)
|
||||
.map_err(|e| JwkError::InvalidEcPublicKey { cause: e.to_string() })?;
|
||||
|
||||
match ec_key.curve() {
|
||||
NamedEcCurve::Known(EcCurve::NistP256) => {
|
||||
let point = p256::Sec1Point::from_bytes(ec_key.encoded_point()).map_err(|_| {
|
||||
JwkError::InvalidEcPublicKey {
|
||||
cause: "invalid P-256 EC point encoding".to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
match (point.x(), point.y()) {
|
||||
(Some(x), Some(y)) => Ok(Self::new(JwkKeyType::new_ec_key(
|
||||
JwkEcPublicKeyCurve::P256,
|
||||
x.as_slice(),
|
||||
y.as_slice(),
|
||||
))),
|
||||
_ => Err(JwkError::InvalidEcPublicKey {
|
||||
cause: "Invalid P-256 curve EC public point coordinates".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
NamedEcCurve::Known(EcCurve::NistP384) => {
|
||||
let point = p384::Sec1Point::from_bytes(ec_key.encoded_point()).map_err(|_| {
|
||||
JwkError::InvalidEcPublicKey {
|
||||
cause: "invalid P-384 EC point encoding".to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
match (point.x(), point.y()) {
|
||||
(Some(x), Some(y)) => Ok(Self::new(JwkKeyType::new_ec_key(
|
||||
JwkEcPublicKeyCurve::P384,
|
||||
x.as_slice(),
|
||||
y.as_slice(),
|
||||
))),
|
||||
_ => Err(JwkError::InvalidEcPublicKey {
|
||||
cause: "Invalid P-384 curve EC public point coordinates".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
NamedEcCurve::Known(EcCurve::NistP521) => {
|
||||
let point = p521::Sec1Point::from_bytes(ec_key.encoded_point()).map_err(|_| {
|
||||
JwkError::InvalidEcPublicKey {
|
||||
cause: "invalid P-521 EC point encoding".to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
match (point.x(), point.y()) {
|
||||
(Some(x), Some(y)) => Ok(Self::new(JwkKeyType::new_ec_key(
|
||||
JwkEcPublicKeyCurve::P521,
|
||||
x.as_slice(),
|
||||
y.as_slice(),
|
||||
))),
|
||||
_ => Err(JwkError::InvalidEcPublicKey {
|
||||
cause: "Invalid P-521 curve EC public point coordinates".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
NamedEcCurve::Unsupported(_) => Err(JwkError::UnsupportedAlgorithm {
|
||||
algorithm: "Unsupported EC curve",
|
||||
}),
|
||||
}
|
||||
}
|
||||
SerdePublicKey::Ed(_) => {
|
||||
let ed_key = EdPublicKey::try_from(public_key)
|
||||
.map_err(|e| JwkError::InvalidEdPublicKey { cause: e.to_string() })?;
|
||||
|
||||
let algorithm = match ed_key.algorithm() {
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::Ed25519) => JwkEdPublicKeyAlgorithm::Ed25519,
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::X25519) => JwkEdPublicKeyAlgorithm::X25519,
|
||||
NamedEdAlgorithm::Unsupported(_) => {
|
||||
return Err(JwkError::UnsupportedAlgorithm {
|
||||
algorithm: "Unsupported ED algorithm",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self::new(JwkKeyType::new_ed_key(algorithm, ed_key.data())))
|
||||
}
|
||||
SerdePublicKey::Mldsa(_) => Err(JwkError::UnsupportedAlgorithm {
|
||||
algorithm: "JWK unsupported with MLDSA keys",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Result<String, JwkError> {
|
||||
Ok(serde_json::to_string(self)?)
|
||||
}
|
||||
|
||||
pub fn to_json_pretty(&self) -> Result<String, JwkError> {
|
||||
Ok(serde_json::to_string_pretty(self)?)
|
||||
}
|
||||
|
||||
pub fn to_public_key(&self) -> Result<PublicKey, JwkError> {
|
||||
match &self.key {
|
||||
JwkKeyType::Rsa(rsa) => {
|
||||
let modulus = IntegerAsn1::from_bytes_be_signed(rsa.modulus_signed_bytes_be()?);
|
||||
let public_exponent = IntegerAsn1::from_bytes_be_signed(rsa.public_exponent_signed_bytes_be()?);
|
||||
let spki = SubjectPublicKeyInfo::new_rsa_key(modulus, public_exponent);
|
||||
Ok(spki.into())
|
||||
}
|
||||
JwkKeyType::Ec(ec) => {
|
||||
let curve = match ec.crv {
|
||||
JwkEcPublicKeyCurve::P256 => EcCurve::NistP256,
|
||||
JwkEcPublicKeyCurve::P384 => EcCurve::NistP384,
|
||||
JwkEcPublicKeyCurve::P521 => EcCurve::NistP521,
|
||||
};
|
||||
|
||||
let x = BoxedUint::from_be_slice_vartime(&ec.x_signed_bytes_be()?);
|
||||
let y = BoxedUint::from_be_slice_vartime(&ec.y_signed_bytes_be()?);
|
||||
|
||||
PublicKey::from_ec_components(curve, &x, &y).map_err(|_| JwkError::InvalidEcPointCoordinates)
|
||||
}
|
||||
JwkKeyType::Ed(ed) => {
|
||||
let algorithm = match ed.crv {
|
||||
JwkEdPublicKeyAlgorithm::Ed25519 => Ok(EdAlgorithm::Ed25519),
|
||||
JwkEdPublicKeyAlgorithm::X25519 => Ok(EdAlgorithm::X25519),
|
||||
JwkEdPublicKeyAlgorithm::Ed448 => Err("ed448 algorithm"),
|
||||
JwkEdPublicKeyAlgorithm::X448 => Err("x448 algorithm"),
|
||||
}
|
||||
.map_err(|algorithm| JwkError::UnsupportedAlgorithm { algorithm })?;
|
||||
|
||||
Ok(PublicKey::from_ed_encoded_components(
|
||||
&algorithm.into(),
|
||||
ed.public_key_bytes()?.as_ref(),
|
||||
))
|
||||
}
|
||||
JwkKeyType::Oct => Err(JwkError::UnsupportedAlgorithm {
|
||||
algorithm: "octet sequence",
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === jwk set === //
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct JwkSet {
|
||||
pub keys: Vec<Jwk>,
|
||||
}
|
||||
|
||||
impl JwkSet {
|
||||
pub fn from_json(json: &str) -> Result<Self, JwkError> {
|
||||
Ok(serde_json::from_str(json)?)
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Result<String, JwkError> {
|
||||
Ok(serde_json::to_string(self)?)
|
||||
}
|
||||
|
||||
pub fn to_json_pretty(&self) -> Result<String, JwkError> {
|
||||
Ok(serde_json::to_string_pretty(self)?)
|
||||
}
|
||||
}
|
||||
|
||||
// === public rsa key === //
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct JwkPublicRsaKey {
|
||||
n: String,
|
||||
e: String,
|
||||
}
|
||||
|
||||
impl JwkPublicRsaKey {
|
||||
pub fn modulus_signed_bytes_be(&self) -> Result<Vec<u8>, JwkError> {
|
||||
let mut buf = h_allocate_signed_big_int_buffer(&self.n);
|
||||
general_purpose::URL_SAFE_NO_PAD
|
||||
.decode_vec(&self.n, &mut buf)
|
||||
.map_err(JwkError::from)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub fn modulus_unsigned_bytes_be(&self) -> Result<Vec<u8>, JwkError> {
|
||||
general_purpose::URL_SAFE_NO_PAD.decode(&self.n).map_err(JwkError::from)
|
||||
}
|
||||
|
||||
pub fn public_exponent_signed_bytes_be(&self) -> Result<Vec<u8>, JwkError> {
|
||||
let mut buf = h_allocate_signed_big_int_buffer(&self.e);
|
||||
general_purpose::URL_SAFE_NO_PAD
|
||||
.decode_vec(&self.e, &mut buf)
|
||||
.map_err(JwkError::from)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub fn public_exponent_unsigned_bytes_be(&self) -> Result<Vec<u8>, JwkError> {
|
||||
general_purpose::URL_SAFE_NO_PAD.decode(&self.e).map_err(JwkError::from)
|
||||
}
|
||||
}
|
||||
|
||||
/// The key type of a JWK defined in
|
||||
/// [RFC 7518, section 6.1](https://tools.ietf.org/html/rfc7518#section-6.1).
|
||||
///
|
||||
/// Note that P521 is not supported yet for signning and verification.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum JwkEcPublicKeyCurve {
|
||||
#[serde(rename = "P-256")]
|
||||
P256,
|
||||
#[serde(rename = "P-384")]
|
||||
P384,
|
||||
#[serde(rename = "P-521")]
|
||||
P521,
|
||||
}
|
||||
|
||||
// === public ec key === //
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct JwkPublicEcKey {
|
||||
crv: JwkEcPublicKeyCurve,
|
||||
x: String,
|
||||
y: String,
|
||||
}
|
||||
|
||||
impl JwkPublicEcKey {
|
||||
pub fn x_signed_bytes_be(&self) -> Result<Vec<u8>, JwkError> {
|
||||
let mut buf = h_allocate_signed_big_int_buffer(&self.x);
|
||||
general_purpose::URL_SAFE_NO_PAD
|
||||
.decode_vec(&self.x, &mut buf)
|
||||
.map_err(JwkError::from)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub fn y_signed_bytes_be(&self) -> Result<Vec<u8>, JwkError> {
|
||||
let mut buf = h_allocate_signed_big_int_buffer(&self.y);
|
||||
general_purpose::URL_SAFE_NO_PAD
|
||||
.decode_vec(&self.y, &mut buf)
|
||||
.map_err(JwkError::from)?;
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// Defined in [RFC 8037](https://tools.ietf.org/html/rfc8037)
|
||||
///
|
||||
/// Note that X25519, Ed448 and X448 are not yet supported by picky for jws/jwe.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum JwkEdPublicKeyAlgorithm {
|
||||
#[serde(rename = "Ed25519")]
|
||||
Ed25519,
|
||||
#[serde(rename = "Ed448")]
|
||||
Ed448,
|
||||
#[serde(rename = "X25519")]
|
||||
X25519,
|
||||
#[serde(rename = "X448")]
|
||||
X448,
|
||||
}
|
||||
|
||||
// === public ed key === //
|
||||
|
||||
/// Defined in [RFC 8037](https://tools.ietf.org/html/rfc8037)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct JwkPublicEdKey {
|
||||
/// NOTE: "crv" defines not exactly the curve but the algorithm.
|
||||
crv: JwkEdPublicKeyAlgorithm,
|
||||
/// In contrast to EC keys, the `x` coordinate is an octet string, not an encoded big integer.
|
||||
x: String,
|
||||
}
|
||||
|
||||
impl JwkPublicEdKey {
|
||||
pub fn public_key_bytes(&self) -> Result<Vec<u8>, JwkError> {
|
||||
let mut buf = Vec::new();
|
||||
general_purpose::URL_SAFE_NO_PAD
|
||||
.decode_vec(&self.x, &mut buf)
|
||||
.map_err(JwkError::from)?;
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::jose::jws::JwsAlg;
|
||||
use crate::pem::Pem;
|
||||
use rstest::rstest;
|
||||
|
||||
const RSA_MODULUS: &str = "rpJjxW0nNZiq1mPC3ZAxqf9qNjmKurP7XuKrpWrfv3IOUldqChQVPNg8zCvDOMZIO-ZDuRmVH\
|
||||
EZ5E1vz5auHNACnpl6AvDGJ-4qyX42vfUDMNZx8i86d7bQpwJkO_MVMLj8qMGmTVbQ8zqVw2z\
|
||||
MyKUFfa2V83nvx2wz4FJh2Thw2uZX2P7h8nlDVSuXO0wJ_OY_2qtqRIAnNXMzL5BF5pEFh4hi\
|
||||
JIFiMTNkhVtUjT1QSB9E8DtDme8g4u769Oc0My45fgqSNE7kKKyaDhTfqSovyhj-qWiD-X_Gw\
|
||||
pWkW4ungpHzz_97-ZDB3yQ7AMwKAsw5EW2cMqseAp3f-kf159w";
|
||||
|
||||
const RSA_PUBLIC_EXPONENT: &str = "AQAB";
|
||||
|
||||
const X509_SHA1_THUMBPRINT: &str = "N3ORVnr9T6opxpS9iRbkKGwKiQI";
|
||||
|
||||
const X509_CERT_0: &str = "MIIDWjCCAkKgAwIBAgIUWRsBqKmpXGP/OwrwLWicwxhuCFowDQYJKoZIhvc\
|
||||
NAQELBQAwKjEoMCYGA1UEAwwfbG9naW4uZGV2b2x1dGlvbnMuY29tIEF1dG\
|
||||
hvcml0eTAeFw0xOTAzMTMxMzE1MzVaFw0yMDAzMTIxMzE1MzVaMCYxJDAiB\
|
||||
gNVBAMMG2xvZ2luLmRldm9sdXRpb25zLmNvbSBUb2tlbjCCASIwDQYJKoZI\
|
||||
hvcNAQEBBQADggEPADCCAQoCggEBAK6SY8VtJzWYqtZjwt2QMan/ajY5irq\
|
||||
z+17iq6Vq379yDlJXagoUFTzYPMwrwzjGSDvmQ7kZlRxGeRNb8+WrhzQAp6\
|
||||
ZegLwxifuKsl+Nr31AzDWcfIvOne20KcCZDvzFTC4/KjBpk1W0PM6lcNszM\
|
||||
ilBX2tlfN578dsM+BSYdk4cNrmV9j+4fJ5Q1UrlztMCfzmP9qrakSAJzVzM\
|
||||
y+QReaRBYeIYiSBYjEzZIVbVI09UEgfRPA7Q5nvIOLu+vTnNDMuOX4KkjRO\
|
||||
5Cismg4U36kqL8oY/qlog/l/xsKVpFuLp4KR88//e/mQwd8kOwDMCgLMORF\
|
||||
tnDKrHgKd3/pH9efcCAwEAAaN8MHowCQYDVR0TBAIwADAOBgNVHQ8BAf8EB\
|
||||
AMCBeAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQW\
|
||||
BBQQW2Cx8HUpXfFM3B76WzBb/BhCBDAfBgNVHSMEGDAWgBRWAUlOiE4Z3ww\
|
||||
aHgz284/sYB9NaDANBgkqhkiG9w0BAQsFAAOCAQEAkliCiJF9Z/Y57V6Rrn\
|
||||
gHCBBWtqR+N/A+KHQqWxP2MmJiHVBBnZAueVPsvykO+EfbazNEkUoPVKhUd\
|
||||
5NxEmTEMOBu9HUEzlmA5xDjl5xS7fejJIr7pgbxIup4m+DsNsPVnF1Snk56\
|
||||
F6660RhRb9fsHQ0pgvWuG+tQXJ4J1Zi0cp+xi4yze6hJGAyAqj6wU46AUiL\
|
||||
6kUr9GUVHqEsl5mNMIW18JT4KM/s5DWxFGO2soSTkaVHwGSkMBQSTgHMWs0\
|
||||
L3bBfimjw9FwjwwHAbe1W5QU6uVXGApuKANRsXxgCn566QkE/BuV3WVR6uy\
|
||||
n2P1J/vU9hxasgRIcjf3jHC4lGpew==";
|
||||
|
||||
const X509_CERT_1: &str = "MIIDRjCCAi6gAwIBAgIUUqhc3/U6OhKtEk1b8JfX3GL0FPYwDQYJKoZIhvc\
|
||||
NAQELBQAwKDEmMCQGA1UEAwwdbG9naW4uZGV2b2x1dGlvbnMuY29tIFJvb3\
|
||||
QgQ0EwHhcNMTkwMzEzMTMxNTM1WhcNMjAwMzEyMTMxNTM1WjAqMSgwJgYDV\
|
||||
QQDDB9sb2dpbi5kZXZvbHV0aW9ucy5jb20gQXV0aG9yaXR5MIIBIjANBgkq\
|
||||
hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlbRwXVPc/WH4t/Yti5qv24pAu8Q\
|
||||
m0eOVvbum23bYtfJDbCSDh7sY/vvQXgIkM8/0C3tFZ3XaXHbyDHAMn6OC+S\
|
||||
Obzs6SjpfKk9s69Yo/aWFl9oRnAK/+dZ0Y6MTdZO1w+PpR81q5QOFMLpWX1\
|
||||
YNdahaZec31sBmsHqlW04OrHUhGOTGdWNots9/PWvN//x++FL+Sqgh/jxF7\
|
||||
khbgfAuz1QKa8P0ZlE4cOcRIs5bSnUFwtoytKH02/YZnCJD7I/iXFuCPV/+\
|
||||
LZO6yobkTREE3npeXvAKr1OKF2F0JVORMhHiYyguh9t3bMwHTCFqmfQkIMD\
|
||||
GjaTJD7bd8y2Au+eDzgwIDAQABo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDV\
|
||||
R0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQUVgFJTohOGd8MGh4M9vOP7GAf\
|
||||
TWgwHwYDVR0jBBgwFoAU42BA1coGHUUPUSeacQfTzicjosgwDQYJKoZIhvc\
|
||||
NAQELBQADggEBAKyyDs+uIughmloEmlf8s1cSP8cLtC1Di2TfYSG0bpEM3B\
|
||||
EPTond/7ujDlv0eug9NRurvWd5v7bWvy9VlJo+x2rLBmkzaNcBSVHZ4UbFU\
|
||||
90MSvHjxNZ7VbUfbWsJVeaYHtqf1m3z0fYT0tUor3chD+wbSqraWw4+t54h\
|
||||
fJl22jExTWS9X0F5/Gf3LQOiOvtjHP+b3VkpXkEPIBbvIO/X6kgoGDLm/lA\
|
||||
IPdZmpI956z5+acLHu3AQkxNXQPzCjSSdJphLVU1XeHXOMWldVtE9BqSMVI\
|
||||
HZ6oCz/FtMA4F6R7WiVXXGR+ywRwFyeiFoRea2ImUK9TRWFsaXKeOBMm+TL\
|
||||
bk=";
|
||||
|
||||
const X509_CERT_2: &str = "MIIDRDCCAiygAwIBAgIUCAKwhsjTttdG4koEAV7zqlnI7wkwDQYJKoZIhvc\
|
||||
NAQELBQAwKDEmMCQGA1UEAwwdbG9naW4uZGV2b2x1dGlvbnMuY29tIFJvb3\
|
||||
QgQ0EwHhcNMTkwMzEzMTMxNTM1WhcNMjQwMzExMTMxNTM1WjAoMSYwJAYDV\
|
||||
QQDDB1sb2dpbi5kZXZvbHV0aW9ucy5jb20gUm9vdCBDQTCCASIwDQYJKoZI\
|
||||
hvcNAQEBBQADggEPADCCAQoCggEBANRZxxg9eTCMVr4DsIUcytQOLnlZ7tl\
|
||||
uliP+jM76mjJEuWqizHzZ1ZoPcEbdW9sV8kgWdPHL3KOlXAr0DEobnhQsNx\
|
||||
uzJ8B73TcV7AKp2HR+xCTKPEha1gVHgQMmzQyCIgLEsdcjhsFeFYqMflELZ\
|
||||
rMy+7DBSZWWf3wCnxiKbzTL01wKqylVWeSiXsniTpsoUSSk8Fe2/Li8dBMY\
|
||||
he1vTb57GI8ta24P4lfJv6CPTNTVsr+6ue3lRuY/UIMNTybhBSc00qbuo0K\
|
||||
ahWHyzDgY+iNEaALbyWeNOoTBQIO8lp4mhHcO/Znh2PxdqCi/FSCB2+A1Xd\
|
||||
uOArn+MKegU5aVJN0CAwEAAaNmMGQwEgYDVR0TAQH/BAgwBgEB/wIBAjAOB\
|
||||
gNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFONgQNXKBh1FD1EnmnEH084nI6LI\
|
||||
MB8GA1UdIwQYMBaAFONgQNXKBh1FD1EnmnEH084nI6LIMA0GCSqGSIb3DQE\
|
||||
BCwUAA4IBAQB+v34Vk/+qQgA7eWlczWNVWM0J67om+QwtMEo+VgzE2OHNID\
|
||||
2o5QXsxcck0j8dANutkoqsUXpos/RG+QPNng5RBWA/sWUYWdfwZgrE30rBK\
|
||||
waP8Yi8gVsZpz3/RClbPcfkUXI12ANw3bRI1TscOK165p1TV6nmeEus5LZq\
|
||||
CJV37/WRt47CccsDNZaqSN7T5lQ045jsZVYpfgx/I1l9Q/fICrTOFwqYbXJ\
|
||||
9DTe1v8C+LFbtTNcEzRGwZefLTNH2yuZjGy1/t4+cnmFJUzmC4abOoZcpkr\
|
||||
z6U68caCbQA+wdmFs4XaO2bFaiyM+m0LVMOQfLuX/0RZc2KB7fAbb7oHQl";
|
||||
|
||||
fn get_jwk_set() -> JwkSet {
|
||||
JwkSet {
|
||||
keys: vec![Jwk {
|
||||
alg: Some(Jwa::Sig(JwsAlg::RS256)),
|
||||
key_ops: Some(vec![JwkKeyOps::Verify]),
|
||||
kid: Some("bG9naW4uZGV2b2x1dGlvbnMuY29tIFRva2VuLk1hciAxMyAxMzoxNTozNSAyMDE5IEdNVA".to_owned()),
|
||||
x5t: Some(X509_SHA1_THUMBPRINT.to_owned()),
|
||||
x5c: Some(vec![
|
||||
X509_CERT_0.to_owned(),
|
||||
X509_CERT_1.to_owned(),
|
||||
X509_CERT_2.to_owned(),
|
||||
]),
|
||||
..Jwk::new(JwkKeyType::new_rsa_key_from_base64_url(
|
||||
RSA_MODULUS.into(),
|
||||
RSA_PUBLIC_EXPONENT.into(),
|
||||
))
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rsa_key() {
|
||||
let expected = get_jwk_set();
|
||||
let decoded = JwkSet::from_json(picky_test_data::JOSE_JWK_SET).unwrap();
|
||||
pretty_assertions::assert_eq!(decoded, expected);
|
||||
|
||||
let encoded = expected.to_json_pretty().unwrap();
|
||||
let decoded = JwkSet::from_json(&encoded).unwrap();
|
||||
pretty_assertions::assert_eq!(decoded, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::JOSE_JWK_EC_P256_JSON)]
|
||||
#[case(picky_test_data::JOSE_JWK_EC_P384_JSON)]
|
||||
#[case(picky_test_data::JOSE_JWK_EC_P521_JSON)]
|
||||
fn ecdsa_key_roundtrip(#[case] json: &str) {
|
||||
let decoded = Jwk::from_json(json).unwrap();
|
||||
let encoded = decoded.to_json().unwrap();
|
||||
pretty_assertions::assert_eq!(encoded, json);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::JOSE_JWK_ED25519_JSON)]
|
||||
#[case(picky_test_data::JOSE_JWK_X25519_JSON)]
|
||||
fn ed_key_roundtrip(#[case] json: &str) {
|
||||
let decoded = Jwk::from_json(json).unwrap();
|
||||
let encoded = decoded.to_json().unwrap();
|
||||
pretty_assertions::assert_eq!(encoded, json);
|
||||
}
|
||||
|
||||
const PUBLIC_KEY_PEM: &str = r#"-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA61BjmfXGEvWmegnBGSuS
|
||||
+rU9soUg2FnODva32D1AqhwdziwHINFaD1MVlcrYG6XRKfkcxnaXGfFDWHLEvNBS
|
||||
EVCgJjtHAGZIm5GL/KA86KDp/CwDFMSwluowcXwDwoyinmeOY9eKyh6aY72xJh7n
|
||||
oLBBq1N0bWi1e2i+83txOCg4yV2oVXhBo8pYEJ8LT3el6Smxol3C1oFMVdwPgc0v
|
||||
Tl25XucMcG/ALE/KNY6pqC2AQ6R2ERlVgPiUWOPatVkt7+Bs3h5Ramxh7XjBOXeu
|
||||
lmCpGSynXNcpZ/06+vofGi/2MlpQZNhHAo8eayMp6FcvNucIpUndo1X8dKMv3Y26
|
||||
ZQIDAQAB
|
||||
-----END PUBLIC KEY-----"#;
|
||||
|
||||
#[test]
|
||||
fn x509_and_jwk_conversion_rsa() {
|
||||
let initial_key = PublicKey::from_pem(&PUBLIC_KEY_PEM.parse::<Pem>().expect("pem")).expect("public key");
|
||||
let jwk = Jwk::from_public_key(&initial_key).unwrap();
|
||||
if let JwkKeyType::Rsa(rsa_key) = &jwk.key {
|
||||
let modulus = general_purpose::URL_SAFE_NO_PAD.decode(&rsa_key.n).unwrap();
|
||||
assert_ne!(modulus[0], 0x00);
|
||||
let public_exponent = general_purpose::URL_SAFE_NO_PAD.decode(&rsa_key.e).unwrap();
|
||||
assert_ne!(public_exponent[0], 0x00);
|
||||
} else {
|
||||
panic!("Unexpected key type");
|
||||
}
|
||||
let from_jwk_key = jwk.to_public_key().unwrap();
|
||||
assert_eq!(from_jwk_key, initial_key);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::EC_NIST256_PK_1_PUB)]
|
||||
#[case(picky_test_data::EC_NIST384_PK_1_PUB)]
|
||||
fn x509_and_jwk_conversion_ec(#[case] pem: &str) {
|
||||
let initial_key = PublicKey::from_pem(&pem.parse::<Pem>().expect("pem")).expect("public key");
|
||||
let jwk = Jwk::from_public_key(&initial_key).unwrap();
|
||||
if let JwkKeyType::Ec(rsa_key) = &jwk.key {
|
||||
let x = general_purpose::URL_SAFE_NO_PAD.decode(&rsa_key.x).unwrap();
|
||||
assert_ne!(x[0], 0x00);
|
||||
let y = general_purpose::URL_SAFE_NO_PAD.decode(&rsa_key.y).unwrap();
|
||||
assert_ne!(y[0], 0x00);
|
||||
} else {
|
||||
panic!("Unexpected key type");
|
||||
}
|
||||
let from_jwk_key = jwk.to_public_key().unwrap();
|
||||
assert_eq!(from_jwk_key, initial_key);
|
||||
}
|
||||
}
|
||||
Vendored
+668
@@ -0,0 +1,668 @@
|
||||
//! JSON Web Signature (JWS) represents content secured with digital signatures or Message Authentication Codes (MACs) using JSON-based data structures.
|
||||
//!
|
||||
//! See [RFC7515](https://tools.ietf.org/html/rfc7515).
|
||||
|
||||
use crate::hash::HashAlgorithm;
|
||||
use crate::jose::jwk::Jwk;
|
||||
use crate::key::{EcCurve, PrivateKey, PublicKey};
|
||||
use crate::signature::{SignatureAlgorithm, SignatureError};
|
||||
use base64::engine::general_purpose;
|
||||
use base64::{DecodeError, Engine as _};
|
||||
use picky_asn1::wrapper::IntegerAsn1;
|
||||
use picky_asn1_x509::signature::EcdsaSignatureValue;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
// === error type === //
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum JwsError {
|
||||
/// RSA error
|
||||
#[error("RSA error: {context}")]
|
||||
Rsa { context: String },
|
||||
|
||||
/// Json error
|
||||
#[error("JSON error: {source}")]
|
||||
Json { source: serde_json::Error },
|
||||
|
||||
/// signature error
|
||||
#[error("signature error: {source}")]
|
||||
Signature { source: SignatureError },
|
||||
|
||||
/// invalid token encoding
|
||||
#[error("input isn't a valid token string: {input}")]
|
||||
InvalidEncoding { input: String },
|
||||
|
||||
/// couldn't decode base64
|
||||
#[error("couldn't decode base64: {source}")]
|
||||
Base64Decoding { source: DecodeError },
|
||||
|
||||
/// input isn't valid utf8
|
||||
#[error("input isn't valid utf8: {source}, input: {input:?}")]
|
||||
InvalidUtf8 {
|
||||
source: std::string::FromUtf8Error,
|
||||
input: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<rsa::errors::Error> for JwsError {
|
||||
fn from(e: rsa::errors::Error) -> Self {
|
||||
Self::Rsa { context: e.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for JwsError {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Self::Json { source: e }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SignatureError> for JwsError {
|
||||
fn from(e: SignatureError) -> Self {
|
||||
Self::Signature { source: e }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DecodeError> for JwsError {
|
||||
fn from(e: DecodeError) -> Self {
|
||||
Self::Base64Decoding { source: e }
|
||||
}
|
||||
}
|
||||
|
||||
// === JWS algorithms === //
|
||||
|
||||
mod jws_alg {
|
||||
// We use #[deprecated] for `JwsAlg::ED22519` to tell users that this algorithm is invalid
|
||||
// (`JwsAlg::EdDSA` should be used instead) but we should keep it inside enum to allow decoding
|
||||
// of invalid tokens generated by other libraries such as `golang-jws`. However, to avoid
|
||||
// compilation warnings caused by enum derives, we need to allow deprecated items inside this
|
||||
// module.
|
||||
#![allow(deprecated)]
|
||||
|
||||
use super::*;
|
||||
|
||||
/// `alg` header parameter values for JWS
|
||||
///
|
||||
/// [JSON Web Algorithms (JWA) draft-ietf-jose-json-web-algorithms-40 #3](https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#section-3.1)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum JwsAlg {
|
||||
/// HMAC using SHA-256 (unsupported)
|
||||
///
|
||||
/// Required by RFC
|
||||
HS256,
|
||||
|
||||
/// HMAC using SHA-384 (unsupported)
|
||||
HS384,
|
||||
|
||||
/// HMAC using SHA-512 (unsupported)
|
||||
HS512,
|
||||
|
||||
/// RSASSA-PKCS-v1_5 using SHA-256
|
||||
///
|
||||
/// Recommended by RFC
|
||||
RS256,
|
||||
|
||||
/// RSASSA-PKCS-v1_5 using SHA-384
|
||||
RS384,
|
||||
|
||||
/// RSASSA-PKCS-v1_5 using SHA-512
|
||||
RS512,
|
||||
|
||||
/// ECDSA using P-256 and SHA-256
|
||||
///
|
||||
/// Recommended+ by RFC
|
||||
ES256,
|
||||
|
||||
/// ECDSA using P-384 and SHA-384
|
||||
ES384,
|
||||
|
||||
/// ECDSA using P-521 and SHA-512
|
||||
ES512,
|
||||
|
||||
/// RSASSA-PSS using SHA-256 and MGF1 with SHA-256 (unsupported)
|
||||
PS256,
|
||||
|
||||
/// RSASSA-PSS using SHA-384 and MGF1 with SHA-384 (unsupported)
|
||||
PS384,
|
||||
|
||||
/// RSASSA-PSS using SHA-512 and MGF1 with SHA-512 (unsupported)
|
||||
PS512,
|
||||
|
||||
/// EdDSA using Ed25519/Ed448
|
||||
EdDSA,
|
||||
|
||||
/// [DO NOT USE] EdDSA using Ed25519
|
||||
///
|
||||
/// This value is used by some popular libraries (e.g. `golang-jwt) instead of `EdDSA` due to
|
||||
/// mistake in the implementation. This value is deprecated and should not be used.
|
||||
#[deprecated(note = "You should not use this value, but it may appear in the wild")]
|
||||
ED25519,
|
||||
}
|
||||
}
|
||||
// Hack to localize #![allow(deprecated)]
|
||||
pub use jws_alg::JwsAlg;
|
||||
|
||||
impl TryFrom<SignatureAlgorithm> for JwsAlg {
|
||||
type Error = SignatureError;
|
||||
|
||||
fn try_from(v: SignatureAlgorithm) -> Result<Self, Self::Error> {
|
||||
match v {
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256) => Ok(Self::RS256),
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384) => Ok(Self::RS384),
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512) => Ok(Self::RS512),
|
||||
SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256) => Ok(Self::ES256),
|
||||
SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384) => Ok(Self::ES384),
|
||||
SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_512) => Ok(Self::ES512),
|
||||
SignatureAlgorithm::Ed25519 => Ok(Self::EdDSA),
|
||||
unsupported => Err(SignatureError::UnsupportedAlgorithm {
|
||||
algorithm: format!("{unsupported:?}"),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<JwsAlg> for SignatureAlgorithm {
|
||||
type Error = SignatureError;
|
||||
|
||||
fn try_from(v: JwsAlg) -> Result<Self, Self::Error> {
|
||||
match v {
|
||||
JwsAlg::RS256 => Ok(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256)),
|
||||
JwsAlg::RS384 => Ok(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384)),
|
||||
JwsAlg::RS512 => Ok(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512)),
|
||||
JwsAlg::ES256 => Ok(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256)),
|
||||
JwsAlg::ES384 => Ok(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384)),
|
||||
JwsAlg::ES512 => Ok(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_512)),
|
||||
JwsAlg::EdDSA => Ok(SignatureAlgorithm::Ed25519),
|
||||
#[allow(deprecated)]
|
||||
JwsAlg::ED25519 => Ok(SignatureAlgorithm::Ed25519),
|
||||
unsupported => Err(SignatureError::UnsupportedAlgorithm {
|
||||
algorithm: format!("{unsupported:?}"),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === JWS header === //
|
||||
|
||||
/// JOSE header of a JWS
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct JwsHeader {
|
||||
// -- specific to JWS -- //
|
||||
/// Algorithm Header
|
||||
///
|
||||
/// identifies the cryptographic algorithm used to secure the JWS.
|
||||
pub alg: JwsAlg,
|
||||
|
||||
// -- common with JWE -- //
|
||||
/// JWK Set URL
|
||||
///
|
||||
/// URI that refers to a resource for a set of JSON-encoded public keys,
|
||||
/// one of which corresponds to the key used to digitally sign the JWS.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub jku: Option<String>,
|
||||
|
||||
/// JSON Web Key
|
||||
///
|
||||
/// The public key that corresponds to the key used to digitally sign the JWS.
|
||||
/// This key is represented as a JSON Web Key (JWK).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub jwk: Option<Jwk>,
|
||||
|
||||
/// Type header
|
||||
///
|
||||
/// Used by JWS applications to declare the media type [IANA.MediaTypes] of this complete JWS.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub typ: Option<String>,
|
||||
|
||||
/// Content Type header
|
||||
///
|
||||
/// Used by JWS applications to declare the media type [IANA.MediaTypes] of the secured content (the payload).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cty: Option<String>,
|
||||
|
||||
// -- common with all -- //
|
||||
/// Key ID Header
|
||||
///
|
||||
/// A hint indicating which key was used.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub kid: Option<String>,
|
||||
|
||||
/// X.509 URL Header
|
||||
///
|
||||
/// URI that refers to a resource for an X.509 public key certificate or certificate chain.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub x5u: Option<String>,
|
||||
|
||||
/// X.509 Certificate Chain
|
||||
///
|
||||
/// Chain of one or more PKIX certificates.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub x5c: Option<Vec<String>>,
|
||||
|
||||
/// X.509 Certificate SHA-1 Thumbprint
|
||||
///
|
||||
/// base64url-encoded SHA-1 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub x5t: Option<String>,
|
||||
|
||||
/// X.509 Certificate SHA-256 Thumbprint
|
||||
///
|
||||
/// base64url-encoded SHA-256 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate.
|
||||
#[serde(rename = "x5t#S256", alias = "x5t#s256", skip_serializing_if = "Option::is_none")]
|
||||
pub x5t_s256: Option<String>,
|
||||
|
||||
// -- extra parameters -- //
|
||||
/// Additional header parameters (both public and private)
|
||||
#[serde(flatten)]
|
||||
pub additional: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
impl JwsHeader {
|
||||
pub fn new(alg: JwsAlg) -> Self {
|
||||
Self {
|
||||
alg,
|
||||
jku: None,
|
||||
jwk: None,
|
||||
typ: None,
|
||||
cty: None,
|
||||
kid: None,
|
||||
x5u: None,
|
||||
x5c: None,
|
||||
x5t: None,
|
||||
x5t_s256: None,
|
||||
additional: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_cty(alg: JwsAlg, cty: impl Into<String>) -> Self {
|
||||
Self {
|
||||
cty: Some(cty.into()),
|
||||
..Self::new(alg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === json web signature === //
|
||||
|
||||
/// Provides an API to sign any kind of data (binary). JSON claims are part of `Jwt` only.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Jws {
|
||||
pub header: JwsHeader,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Jws {
|
||||
pub fn new(alg: JwsAlg, payload: Vec<u8>) -> Self {
|
||||
Self {
|
||||
header: JwsHeader::new(alg),
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode(&self, private_key: &PrivateKey) -> Result<String, JwsError> {
|
||||
let header_base64 = general_purpose::URL_SAFE_NO_PAD.encode(serde_json::to_vec(&self.header)?);
|
||||
let payload_base64 = general_purpose::URL_SAFE_NO_PAD.encode(&self.payload);
|
||||
let header_and_payload = [header_base64, payload_base64].join(".");
|
||||
let signature_algo = SignatureAlgorithm::try_from(self.header.alg)?;
|
||||
let signature = signature_algo.sign(header_and_payload.as_bytes(), private_key)?;
|
||||
|
||||
let signature = match self.header.alg {
|
||||
// EC sugnatures have special encoding rules (RFC 7518, section 3.4)
|
||||
JwsAlg::ES256 | JwsAlg::ES384 | JwsAlg::ES512 => {
|
||||
// Parse signature as ASN.1 DER sequence
|
||||
let signature: EcdsaSignatureValue =
|
||||
picky_asn1_der::from_bytes(&signature).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Invalid EC DER signature encoding: {e}"),
|
||||
})?;
|
||||
|
||||
let curve = match self.header.alg {
|
||||
JwsAlg::ES256 => EcCurve::NistP256,
|
||||
JwsAlg::ES384 => EcCurve::NistP384,
|
||||
JwsAlg::ES512 => EcCurve::NistP521,
|
||||
_ => unreachable!("Checked in match above"),
|
||||
};
|
||||
|
||||
let signature_component_size = curve.field_bytes_size();
|
||||
|
||||
let r = signature.r.as_unsigned_bytes_be();
|
||||
let s = signature.s.as_unsigned_bytes_be();
|
||||
|
||||
// We should add zero padding (leading zeros) for R & S components to match the
|
||||
// size of the curve, as ASN.1 DER encoding removes leading zeros.
|
||||
let mut jws_signature = Vec::with_capacity(signature_component_size * 2);
|
||||
|
||||
let r_padding = signature_component_size - r.len();
|
||||
(0..r_padding).for_each(|_| jws_signature.push(0));
|
||||
jws_signature.extend_from_slice(r);
|
||||
|
||||
let s_padding = signature_component_size - s.len();
|
||||
(0..s_padding).for_each(|_| jws_signature.push(0));
|
||||
jws_signature.extend_from_slice(s);
|
||||
|
||||
jws_signature
|
||||
}
|
||||
_ => signature,
|
||||
};
|
||||
|
||||
let signature_base64 = general_purpose::URL_SAFE_NO_PAD.encode(signature);
|
||||
Ok([header_and_payload, signature_base64].join("."))
|
||||
}
|
||||
|
||||
/// Verifies signature and returns decoded JWS payload.
|
||||
pub fn decode(encoded_token: &str, public_key: &PublicKey) -> Result<Self, JwsError> {
|
||||
RawJws::decode(encoded_token).and_then(|raw_jws| raw_jws.verify(public_key))
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw low-level interface to the yet to be verified JWS token.
|
||||
///
|
||||
/// This is useful to inspect the structure before performing further processing.
|
||||
/// For most usecases, use `Jws` directly.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RawJws<'repr> {
|
||||
pub compact_repr: Cow<'repr, str>,
|
||||
pub header: JwsHeader,
|
||||
payload: Vec<u8>,
|
||||
pub signature: Vec<u8>,
|
||||
}
|
||||
|
||||
/// An owned `RawJws` for convenience.
|
||||
pub type OwnedRawJws = RawJws<'static>;
|
||||
|
||||
impl<'repr> RawJws<'repr> {
|
||||
/// Decodes a JWS in compact representation.
|
||||
pub fn decode(compact_repr: impl Into<Cow<'repr, str>>) -> Result<Self, JwsError> {
|
||||
decode_impl(compact_repr.into())
|
||||
}
|
||||
|
||||
/// Peeks the payload before signature verification.
|
||||
pub fn peek_payload(&self) -> &[u8] {
|
||||
&self.payload
|
||||
}
|
||||
|
||||
/// Verifies signature and returns a verified `Jws` structure.
|
||||
pub fn verify(self, public_key: &PublicKey) -> Result<Jws, JwsError> {
|
||||
verify_signature(&self.compact_repr, public_key, self.header.alg)?;
|
||||
Ok(self.discard_signature())
|
||||
}
|
||||
|
||||
/// Discards the signature without verifying it and hands a `Jws` structure.
|
||||
///
|
||||
/// Generally, you should not do that.
|
||||
pub fn discard_signature(self) -> Jws {
|
||||
Jws {
|
||||
header: self.header,
|
||||
payload: self.payload,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_impl(compact_repr: Cow<'_, str>) -> Result<RawJws<'_>, JwsError> {
|
||||
let first_dot_idx = compact_repr.find('.').ok_or_else(|| JwsError::InvalidEncoding {
|
||||
input: compact_repr.clone().into_owned(),
|
||||
})?;
|
||||
|
||||
let last_dot_idx = compact_repr.rfind('.').ok_or_else(|| JwsError::InvalidEncoding {
|
||||
input: compact_repr.clone().into_owned(),
|
||||
})?;
|
||||
|
||||
if first_dot_idx == last_dot_idx || compact_repr.starts_with('.') || compact_repr.ends_with('.') {
|
||||
return Err(JwsError::InvalidEncoding {
|
||||
input: compact_repr.into_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
let header_json = general_purpose::URL_SAFE_NO_PAD.decode(&compact_repr[..first_dot_idx])?;
|
||||
let header = serde_json::from_slice::<JwsHeader>(&header_json)?;
|
||||
|
||||
let signature = general_purpose::URL_SAFE_NO_PAD.decode(&compact_repr[last_dot_idx + 1..])?;
|
||||
|
||||
let payload = general_purpose::URL_SAFE_NO_PAD.decode(&compact_repr[first_dot_idx + 1..last_dot_idx])?;
|
||||
|
||||
Ok(RawJws {
|
||||
compact_repr,
|
||||
header,
|
||||
payload,
|
||||
signature,
|
||||
})
|
||||
}
|
||||
|
||||
/// JWS verification primitive
|
||||
pub fn verify_signature(encoded_token: &str, public_key: &PublicKey, algorithm: JwsAlg) -> Result<(), JwsError> {
|
||||
let last_dot_idx = encoded_token.rfind('.').ok_or_else(|| JwsError::InvalidEncoding {
|
||||
input: encoded_token.to_owned(),
|
||||
})?;
|
||||
|
||||
if encoded_token.ends_with('.') {
|
||||
return Err(JwsError::InvalidEncoding {
|
||||
input: encoded_token.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
let signature = general_purpose::URL_SAFE_NO_PAD.decode(&encoded_token[last_dot_idx + 1..])?;
|
||||
let signature_algo = SignatureAlgorithm::try_from(algorithm)?;
|
||||
|
||||
let signature = match algorithm {
|
||||
// Special decoding rules for ECDSA
|
||||
JwsAlg::ES256 | JwsAlg::ES384 | JwsAlg::ES512 => {
|
||||
let curve = match algorithm {
|
||||
JwsAlg::ES256 => EcCurve::NistP256,
|
||||
JwsAlg::ES384 => EcCurve::NistP384,
|
||||
JwsAlg::ES512 => EcCurve::NistP521,
|
||||
_ => unreachable!("Checked in match above"),
|
||||
};
|
||||
|
||||
let component_size = curve.field_bytes_size();
|
||||
let jws_encoded_signature_size = component_size * 2;
|
||||
|
||||
if signature.len() != jws_encoded_signature_size {
|
||||
return Err(SignatureError::Ec {
|
||||
context: format!(
|
||||
"Invalid JWS EC signature size. Expected: {}; Actual: {}",
|
||||
jws_encoded_signature_size,
|
||||
signature.len()
|
||||
),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
let (r, s) = signature.split_at(component_size);
|
||||
|
||||
let signature = EcdsaSignatureValue {
|
||||
r: IntegerAsn1::from_bytes_be_unsigned(r.to_vec()),
|
||||
s: IntegerAsn1::from_bytes_be_unsigned(s.to_vec()),
|
||||
};
|
||||
|
||||
picky_asn1_der::to_vec(&signature).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Failed to encode EC signature to DER format: {e}"),
|
||||
})?
|
||||
}
|
||||
_ => signature,
|
||||
};
|
||||
|
||||
signature_algo.verify(public_key, &encoded_token.as_bytes()[..last_dot_idx], &signature)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::pem::Pem;
|
||||
use rstest::rstest;
|
||||
|
||||
const PAYLOAD: &str = r#"{"sub":"1234567890","name":"John Doe","admin":true,"iat":1516239022}"#;
|
||||
|
||||
fn get_private_key_1() -> PrivateKey {
|
||||
let pk_pem = picky_test_data::RSA_2048_PK_1.parse::<Pem>().unwrap();
|
||||
PrivateKey::from_pem(&pk_pem).unwrap()
|
||||
}
|
||||
|
||||
fn get_private_key_2() -> PrivateKey {
|
||||
let pk_pem = picky_test_data::RSA_2048_PK_7.parse::<Pem>().unwrap();
|
||||
PrivateKey::from_pem(&pk_pem).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_rsa_sha256() {
|
||||
let jwt = Jws {
|
||||
header: JwsHeader {
|
||||
typ: Some(String::from("JWT")),
|
||||
..JwsHeader::new(JwsAlg::RS256)
|
||||
},
|
||||
payload: PAYLOAD.as_bytes().to_vec(),
|
||||
};
|
||||
let encoded = jwt.encode(&get_private_key_1()).unwrap();
|
||||
assert_eq!(encoded, picky_test_data::JOSE_JWT_SIG_EXAMPLE);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(JwsAlg::ES256, picky_test_data::EC_NIST256_PK_1)]
|
||||
#[case(JwsAlg::ES384, picky_test_data::EC_NIST384_PK_1)]
|
||||
#[case(JwsAlg::ES512, picky_test_data::EC_NIST521_PK_1)]
|
||||
fn ecdsa_sign_verify(#[case] alg: JwsAlg, #[case] key_pem: &str) {
|
||||
let key = PrivateKey::from_pem_str(key_pem).unwrap();
|
||||
|
||||
let jwt = Jws {
|
||||
header: JwsHeader {
|
||||
typ: Some(String::from("JWT")),
|
||||
..JwsHeader::new(alg)
|
||||
},
|
||||
payload: PAYLOAD.as_bytes().to_vec(),
|
||||
};
|
||||
|
||||
// Check encode + sign
|
||||
let encoded = jwt.encode(&key).unwrap();
|
||||
|
||||
// Check decode + verify
|
||||
let jws = RawJws::decode(&encoded).unwrap();
|
||||
jws.clone().verify(&key.to_public_key().unwrap()).unwrap();
|
||||
|
||||
assert_eq!(&jws.header, &jwt.header);
|
||||
assert_eq!(&jws.payload, &jwt.payload);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::EC_NIST256_PK_1, picky_test_data::JOSE_JWT_SIG_ES256)]
|
||||
#[case(picky_test_data::EC_NIST384_PK_1, picky_test_data::JOSE_JWT_SIG_ES384)]
|
||||
#[case(picky_test_data::EC_NIST521_PK_1, picky_test_data::JOSE_JWT_SIG_ES512)]
|
||||
fn ecdsa_parse_and_verify(#[case] key_pem: &str, #[case] signature: &str) {
|
||||
let key = PrivateKey::from_pem_str(key_pem).unwrap();
|
||||
|
||||
// Check decode + verify
|
||||
let jws = RawJws::decode(signature).unwrap();
|
||||
jws.clone().verify(&key.to_public_key().unwrap()).unwrap();
|
||||
}
|
||||
|
||||
const JWT_ED25519_BODY: &str = r#"{"username":"kataras"}"#;
|
||||
const JWT_ED25519_GO_BODY: &str = r#"{"foo":"bar"}"#;
|
||||
|
||||
/// Validate that invalid header with alg set to "ED25519" could be parsed by picky
|
||||
#[rstest]
|
||||
#[allow(deprecated)]
|
||||
#[case(
|
||||
picky_test_data::JOSE_JWT_SIG_ED25519_GO_PRIVATE_KEY,
|
||||
picky_test_data::JOSE_JWT_SIG_ED25519_GO,
|
||||
JwsAlg::ED25519,
|
||||
JWT_ED25519_GO_BODY
|
||||
)]
|
||||
#[case(
|
||||
picky_test_data::JOSE_JWT_SIG_ED25519_PRIVATE_KEY,
|
||||
picky_test_data::JOSE_JWT_SIG_ED25519,
|
||||
JwsAlg::EdDSA,
|
||||
JWT_ED25519_BODY
|
||||
)]
|
||||
fn ed25519_algorithm(#[case] key: &str, #[case] encoded_expected: &str, #[case] alg: JwsAlg, #[case] body: &str) {
|
||||
let key = PrivateKey::from_pem_str(key).unwrap();
|
||||
|
||||
let jwt = Jws {
|
||||
header: JwsHeader {
|
||||
typ: Some(String::from("JWT")),
|
||||
..JwsHeader::new(alg)
|
||||
},
|
||||
payload: body.as_bytes().to_vec(),
|
||||
};
|
||||
|
||||
// Check encode + sign
|
||||
let encoded = jwt.encode(&key).unwrap();
|
||||
assert_eq!(encoded, encoded_expected);
|
||||
|
||||
// Check decode + verify
|
||||
let jws = RawJws::decode(&encoded).unwrap();
|
||||
jws.verify(&key.to_public_key().unwrap()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rsa_sha256() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
let jwt = Jws::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key).unwrap();
|
||||
assert_eq!(jwt.payload.as_slice(), PAYLOAD.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rsa_sha256_delayed_signature_check() {
|
||||
let jws = RawJws::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE).unwrap();
|
||||
println!("{}", String::from_utf8_lossy(&jws.payload));
|
||||
assert_eq!(jws.peek_payload(), PAYLOAD.as_bytes());
|
||||
|
||||
let public_key = get_private_key_2().to_public_key().unwrap();
|
||||
let err = jws.verify(&public_key).err().unwrap();
|
||||
assert_eq!(err.to_string(), "signature error: invalid signature");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rsa_sha256_invalid_signature_err() {
|
||||
let public_key = get_private_key_2().to_public_key().unwrap();
|
||||
let err = Jws::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key)
|
||||
.err()
|
||||
.unwrap();
|
||||
assert_eq!(err.to_string(), "signature error: invalid signature");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_invalid_base64_err() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
let err = Jws::decode("aieoè~†.tésp.à", &public_key).err().unwrap();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"couldn\'t decode base64: Invalid symbol 195, offset 4."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_invalid_json_err() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
|
||||
let err = Jws::decode("abc.abc.abc", &public_key).err().unwrap();
|
||||
assert_eq!(err.to_string(), "JSON error: expected value at line 1 column 1");
|
||||
|
||||
let err = Jws::decode("eyAiYWxnIjogIkhTMjU2IH0K.abc.abc", &public_key)
|
||||
.err()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"JSON error: control character (\\u0000-\\u001F) \
|
||||
found while parsing a string at line 2 column 0"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_invalid_encoding_err() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
|
||||
let err = Jws::decode(".abc.abc", &public_key).err().unwrap();
|
||||
assert_eq!(err.to_string(), "input isn\'t a valid token string: .abc.abc");
|
||||
|
||||
let err = Jws::decode("abc.abc.", &public_key).err().unwrap();
|
||||
assert_eq!(err.to_string(), "input isn\'t a valid token string: abc.abc.");
|
||||
|
||||
let err = Jws::decode("abc.abc", &public_key).err().unwrap();
|
||||
assert_eq!(err.to_string(), "input isn\'t a valid token string: abc.abc");
|
||||
|
||||
let err = Jws::decode("abc", &public_key).err().unwrap();
|
||||
assert_eq!(err.to_string(), "input isn\'t a valid token string: abc");
|
||||
}
|
||||
}
|
||||
Vendored
+690
@@ -0,0 +1,690 @@
|
||||
use super::jwe::Jwe;
|
||||
use crate::jose::jwe::{JweAlg, JweEnc, JweError, JweHeader};
|
||||
use crate::jose::jws::{Jws, JwsAlg, JwsError, JwsHeader};
|
||||
use crate::key::{PrivateKey, PublicKey};
|
||||
use core::fmt;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use thiserror::Error;
|
||||
|
||||
// === error type === //
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum JwtError {
|
||||
/// JWS error
|
||||
#[error("JWS error: {source}")]
|
||||
Jws { source: JwsError },
|
||||
|
||||
/// JWE error
|
||||
#[error("JWE error: {source}")]
|
||||
Jwe { source: JweError },
|
||||
|
||||
/// Json error
|
||||
#[error("JSON error: {source}")]
|
||||
Json { source: serde_json::Error },
|
||||
|
||||
/// registered claim type is invalid
|
||||
#[error("registered claim `{claim}` has invalid type")]
|
||||
InvalidRegisteredClaimType { claim: &'static str },
|
||||
|
||||
/// a required claim is missing
|
||||
#[error("required claim `{claim}` is missing")]
|
||||
RequiredClaimMissing { claim: &'static str },
|
||||
|
||||
/// token not yet valid
|
||||
#[error("token not yet valid (not before: {}, now: {} [leeway: {}])", not_before, now.numeric_date, now.leeway)]
|
||||
NotYetValid { not_before: i64, now: JwtDate },
|
||||
|
||||
/// token expired
|
||||
#[error("token expired (not after: {}, now: {} [leeway: {}])", not_after, now.numeric_date, now.leeway)]
|
||||
Expired { not_after: i64, now: JwtDate },
|
||||
|
||||
/// validator is invalid
|
||||
#[error("invalid validator: {description}")]
|
||||
InvalidValidator { description: &'static str },
|
||||
}
|
||||
|
||||
impl From<JwsError> for JwtError {
|
||||
fn from(s: JwsError) -> Self {
|
||||
Self::Jws { source: s }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for JwtError {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Self::Json { source: e }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JweError> for JwtError {
|
||||
fn from(s: JweError) -> Self {
|
||||
Self::Jwe { source: s }
|
||||
}
|
||||
}
|
||||
|
||||
// === Validation states === //
|
||||
|
||||
pub struct CheckedState<C> {
|
||||
pub claims: C,
|
||||
}
|
||||
|
||||
impl<C> Clone for CheckedState<C>
|
||||
where
|
||||
C: Clone,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
claims: self.claims.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> fmt::Debug for CheckedState<C>
|
||||
where
|
||||
C: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "ValidatedClaims({:?})", self.claims)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UncheckedState {
|
||||
payload: Vec<u8>,
|
||||
}
|
||||
|
||||
// === JWT date === //
|
||||
|
||||
/// Represent date as defined by [RFC7519](https://tools.ietf.org/html/rfc7519#section-2).
|
||||
///
|
||||
/// A leeway can be configured to account clock skew when comparing with another date.
|
||||
/// Should be small (less than 120).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct JwtDate {
|
||||
pub numeric_date: i64,
|
||||
pub leeway: u16,
|
||||
}
|
||||
|
||||
impl JwtDate {
|
||||
pub const fn new(numeric_date: i64) -> Self {
|
||||
Self {
|
||||
numeric_date,
|
||||
leeway: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn new_with_leeway(numeric_date: i64, leeway: u16) -> Self {
|
||||
Self { numeric_date, leeway }
|
||||
}
|
||||
|
||||
pub const fn is_before(&self, other_numeric_date: i64) -> bool {
|
||||
self.numeric_date <= other_numeric_date + self.leeway as i64
|
||||
}
|
||||
|
||||
pub const fn is_before_strict(&self, other_numeric_date: i64) -> bool {
|
||||
self.numeric_date < other_numeric_date + self.leeway as i64
|
||||
}
|
||||
|
||||
pub const fn is_after(&self, other_numeric_date: i64) -> bool {
|
||||
self.numeric_date >= other_numeric_date - self.leeway as i64
|
||||
}
|
||||
|
||||
pub const fn is_after_strict(&self, other_numeric_date: i64) -> bool {
|
||||
self.numeric_date > other_numeric_date - self.leeway as i64
|
||||
}
|
||||
}
|
||||
|
||||
// === validator === //
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum CheckStrictness {
|
||||
Ignored,
|
||||
Optional,
|
||||
Required,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JwtValidator {
|
||||
current_date: Option<JwtDate>,
|
||||
expiration_claim: CheckStrictness,
|
||||
not_before_claim: CheckStrictness,
|
||||
}
|
||||
|
||||
pub const NO_CHECK_VALIDATOR: JwtValidator = JwtValidator::no_check();
|
||||
|
||||
impl JwtValidator {
|
||||
/// Check signature and the registered exp and nbf claims. If a claim is missing token is rejected.
|
||||
pub const fn strict(current_date: JwtDate) -> Self {
|
||||
Self {
|
||||
current_date: Some(current_date),
|
||||
expiration_claim: CheckStrictness::Required,
|
||||
not_before_claim: CheckStrictness::Required,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check signature and the registered exp and nbf claims. Token isn't rejected if a claim is missing.
|
||||
pub const fn lenient(current_date: JwtDate) -> Self {
|
||||
Self {
|
||||
current_date: Some(current_date),
|
||||
expiration_claim: CheckStrictness::Optional,
|
||||
not_before_claim: CheckStrictness::Optional,
|
||||
}
|
||||
}
|
||||
|
||||
/// No check.
|
||||
pub const fn no_check() -> Self {
|
||||
Self {
|
||||
current_date: None,
|
||||
expiration_claim: CheckStrictness::Ignored,
|
||||
not_before_claim: CheckStrictness::Ignored,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_date(self, current_date: JwtDate) -> Self {
|
||||
Self {
|
||||
current_date: Some(current_date),
|
||||
expiration_claim: CheckStrictness::Required,
|
||||
not_before_claim: CheckStrictness::Required,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expiration_check_required(self) -> Self {
|
||||
Self {
|
||||
expiration_claim: CheckStrictness::Required,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expiration_check_optional(self) -> Self {
|
||||
Self {
|
||||
expiration_claim: CheckStrictness::Optional,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expiration_check_ignored(self) -> Self {
|
||||
Self {
|
||||
expiration_claim: CheckStrictness::Ignored,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not_before_check_required(self) -> Self {
|
||||
Self {
|
||||
not_before_claim: CheckStrictness::Required,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not_before_check_optional(self) -> Self {
|
||||
Self {
|
||||
not_before_claim: CheckStrictness::Optional,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not_before_check_ignored(self) -> Self {
|
||||
Self {
|
||||
not_before_claim: CheckStrictness::Ignored,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === JWT === //
|
||||
|
||||
const JWT_TYPE: &str = "JWT";
|
||||
const EXPIRATION_TIME_CLAIM: &str = "exp";
|
||||
const NOT_BEFORE_CLAIM: &str = "nbf";
|
||||
|
||||
pub struct Jwt<H, State> {
|
||||
pub header: H,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
pub type JwtSig = Jwt<JwsHeader, UncheckedState>;
|
||||
pub type CheckedJwtSig<C> = Jwt<JwsHeader, CheckedState<C>>;
|
||||
pub type JwtEnc = Jwt<JweHeader, UncheckedState>;
|
||||
pub type CheckedJwtEnc<C> = Jwt<JweHeader, CheckedState<C>>;
|
||||
|
||||
impl<H, State> Clone for Jwt<H, State>
|
||||
where
|
||||
H: Clone,
|
||||
State: Clone,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
header: self.header.clone(),
|
||||
state: self.state.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, State> fmt::Debug for Jwt<H, State>
|
||||
where
|
||||
H: fmt::Debug,
|
||||
State: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt.debug_struct("Jwt")
|
||||
.field("header", &self.header)
|
||||
.field("state", &self.state)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, C> Jwt<H, CheckedState<C>> {
|
||||
pub fn new_with_header(header: H, claims: C) -> Self {
|
||||
Jwt {
|
||||
header,
|
||||
state: CheckedState { claims },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> CheckedJwtSig<C> {
|
||||
pub fn new(alg: JwsAlg, claims: C) -> Self {
|
||||
Jwt {
|
||||
header: JwsHeader {
|
||||
typ: Some(JWT_TYPE.to_owned()),
|
||||
..JwsHeader::new(alg)
|
||||
},
|
||||
state: CheckedState { claims },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_cty(alg: JwsAlg, cty: impl Into<String>, claims: C) -> Self {
|
||||
Jwt {
|
||||
header: JwsHeader {
|
||||
typ: Some(JWT_TYPE.to_owned()),
|
||||
..JwsHeader::new_with_cty(alg, cty)
|
||||
},
|
||||
state: CheckedState { claims },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> CheckedJwtSig<C>
|
||||
where
|
||||
C: Serialize,
|
||||
{
|
||||
pub fn encode(self, private_key: &PrivateKey) -> Result<String, JwtError> {
|
||||
let jws = Jws {
|
||||
header: self.header,
|
||||
payload: serde_json::to_vec(&self.state.claims)?,
|
||||
};
|
||||
let encoded = jws.encode(private_key)?;
|
||||
Ok(encoded)
|
||||
}
|
||||
}
|
||||
|
||||
impl JwtSig {
|
||||
pub fn encode(self, private_key: &PrivateKey) -> Result<String, JwtError> {
|
||||
let jws = Jws {
|
||||
header: self.header,
|
||||
payload: self.state.payload,
|
||||
};
|
||||
let encoded = jws.encode(private_key)?;
|
||||
Ok(encoded)
|
||||
}
|
||||
}
|
||||
|
||||
impl JwtSig {
|
||||
/// Verifies signature and returns decoded JWS payload.
|
||||
pub fn decode(encoded_token: &str, public_key: &PublicKey) -> Result<Self, JwtError> {
|
||||
let jws = Jws::decode(encoded_token, public_key)?;
|
||||
Ok(Self::from(jws))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Jws> for JwtSig {
|
||||
fn from(jws: Jws) -> Self {
|
||||
Self {
|
||||
header: jws.header,
|
||||
state: UncheckedState { payload: jws.payload },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> CheckedJwtEnc<C> {
|
||||
pub fn new(alg: JweAlg, enc: JweEnc, claims: C) -> Self {
|
||||
Jwt {
|
||||
header: JweHeader {
|
||||
typ: Some(JWT_TYPE.to_owned()),
|
||||
..JweHeader::new(alg, enc)
|
||||
},
|
||||
state: CheckedState { claims },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_cty(alg: JweAlg, enc: JweEnc, cty: impl Into<String>, claims: C) -> Self {
|
||||
Jwt {
|
||||
header: JweHeader {
|
||||
typ: Some(JWT_TYPE.to_owned()),
|
||||
..JweHeader::new_with_cty(alg, enc, cty)
|
||||
},
|
||||
state: CheckedState { claims },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> CheckedJwtEnc<C>
|
||||
where
|
||||
C: Serialize,
|
||||
{
|
||||
/// Encode with CEK encrypted and included in the token using asymmetric cryptography.
|
||||
pub fn encode(self, asymmetric_key: &PublicKey) -> Result<String, JwtError> {
|
||||
let jwe = Jwe {
|
||||
header: self.header,
|
||||
payload: serde_json::to_vec(&self.state.claims)?,
|
||||
};
|
||||
let encoded = jwe.encode(asymmetric_key)?;
|
||||
Ok(encoded)
|
||||
}
|
||||
|
||||
/// Encode with provided CEK (a symmetric key). This will ignore `alg` value and override it with "dir".
|
||||
pub fn encode_direct(self, cek: &[u8]) -> Result<String, JweError> {
|
||||
let jwe = Jwe {
|
||||
header: self.header,
|
||||
payload: serde_json::to_vec(&self.state.claims)?,
|
||||
};
|
||||
let encoded = jwe.encode_direct(cek)?;
|
||||
Ok(encoded)
|
||||
}
|
||||
}
|
||||
|
||||
impl JwtEnc {
|
||||
/// Decode using asymmetric cryptography.
|
||||
pub fn decode(encoded_token: &str, key: &PrivateKey) -> Result<Self, JwtError> {
|
||||
let jwe = Jwe::decode(encoded_token, key)?;
|
||||
Ok(Self::from(jwe))
|
||||
}
|
||||
|
||||
/// Decode with provided CEK (a symmetric key).
|
||||
pub fn decode_direct(encoded_token: &str, cek: &[u8]) -> Result<Self, JwtError> {
|
||||
let jwe = Jwe::decode_direct(encoded_token, cek)?;
|
||||
Ok(Self::from(jwe))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Jwe> for JwtEnc {
|
||||
fn from(jwe: Jwe) -> Self {
|
||||
Self {
|
||||
header: jwe.header,
|
||||
state: UncheckedState { payload: jwe.payload },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H> Jwt<H, UncheckedState> {
|
||||
/// Validate JWT claims using validator and convert payload to a user-defined typed struct.
|
||||
pub fn validate<C>(self, validator: &JwtValidator) -> Result<Jwt<H, CheckedState<C>>, JwtError>
|
||||
where
|
||||
C: DeserializeOwned,
|
||||
{
|
||||
Ok(Jwt {
|
||||
header: self.header,
|
||||
state: CheckedState {
|
||||
claims: h_decode_and_validate_claims(&self.state.payload, validator)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn h_decode_and_validate_claims<C: DeserializeOwned>(
|
||||
claims_json: &[u8],
|
||||
validator: &JwtValidator,
|
||||
) -> Result<C, JwtError> {
|
||||
let claims = match (
|
||||
&validator.current_date,
|
||||
validator.not_before_claim,
|
||||
validator.expiration_claim,
|
||||
) {
|
||||
(None, CheckStrictness::Required, _) | (None, _, CheckStrictness::Required) => {
|
||||
return Err(JwtError::InvalidValidator {
|
||||
description: "current date is missing",
|
||||
});
|
||||
}
|
||||
(Some(current_date), nbf_strictness, exp_strictness) => {
|
||||
let claims = serde_json::from_slice::<serde_json::Value>(claims_json)?;
|
||||
|
||||
let nbf_opt = claims.get(NOT_BEFORE_CLAIM);
|
||||
match (nbf_strictness, nbf_opt) {
|
||||
(CheckStrictness::Ignored, _) | (CheckStrictness::Optional, None) => {}
|
||||
(CheckStrictness::Required, None) => {
|
||||
return Err(JwtError::RequiredClaimMissing {
|
||||
claim: NOT_BEFORE_CLAIM,
|
||||
});
|
||||
}
|
||||
(_, Some(nbf)) => {
|
||||
let nbf_i64 = nbf.as_i64().ok_or(JwtError::InvalidRegisteredClaimType {
|
||||
claim: NOT_BEFORE_CLAIM,
|
||||
})?;
|
||||
if !current_date.is_after(nbf_i64) {
|
||||
return Err(JwtError::NotYetValid {
|
||||
not_before: nbf_i64,
|
||||
now: current_date.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let exp_opt = claims.get(EXPIRATION_TIME_CLAIM);
|
||||
match (exp_strictness, exp_opt) {
|
||||
(CheckStrictness::Ignored, _) | (CheckStrictness::Optional, None) => {}
|
||||
(CheckStrictness::Required, None) => {
|
||||
return Err(JwtError::RequiredClaimMissing {
|
||||
claim: EXPIRATION_TIME_CLAIM,
|
||||
});
|
||||
}
|
||||
(_, Some(exp)) => {
|
||||
let exp_i64 = exp.as_i64().ok_or(JwtError::InvalidRegisteredClaimType {
|
||||
claim: EXPIRATION_TIME_CLAIM,
|
||||
})?;
|
||||
if !current_date.is_before_strict(exp_i64) {
|
||||
return Err(JwtError::Expired {
|
||||
not_after: exp_i64,
|
||||
now: current_date.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::value::from_value(claims)?
|
||||
}
|
||||
(None, _, _) => serde_json::from_slice(claims_json)?,
|
||||
};
|
||||
|
||||
Ok(claims)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::jose::jws::RawJws;
|
||||
use crate::pem::Pem;
|
||||
use serde::Deserialize;
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
struct MyClaims {
|
||||
sub: Cow<'static, str>,
|
||||
name: Cow<'static, str>,
|
||||
admin: bool,
|
||||
iat: i32,
|
||||
}
|
||||
|
||||
const fn get_strongly_typed_claims() -> MyClaims {
|
||||
MyClaims {
|
||||
sub: Cow::Borrowed("1234567890"),
|
||||
name: Cow::Borrowed("John Doe"),
|
||||
admin: true,
|
||||
iat: 1516239022,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_private_key_1() -> PrivateKey {
|
||||
let pk_pem = picky_test_data::RSA_2048_PK_1.parse::<Pem>().unwrap();
|
||||
PrivateKey::from_pkcs8(pk_pem.data()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_jws_rsa_sha256() {
|
||||
let claims = get_strongly_typed_claims();
|
||||
let jwt = CheckedJwtSig::new(JwsAlg::RS256, claims);
|
||||
let encoded = jwt.encode(&get_private_key_1()).unwrap();
|
||||
assert_eq!(encoded, picky_test_data::JOSE_JWT_SIG_EXAMPLE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_jws_rsa_sha256() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
let jwt = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key)
|
||||
.unwrap()
|
||||
.validate::<MyClaims>(&JwtValidator::no_check())
|
||||
.unwrap();
|
||||
assert_eq!(jwt.state.claims, get_strongly_typed_claims());
|
||||
|
||||
// exp and nbf claims aren't present but this should pass with lenient validator
|
||||
let now = JwtDate::new(0);
|
||||
JwtSig::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key)
|
||||
.unwrap()
|
||||
.validate::<MyClaims>(&JwtValidator::lenient(now))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_jws_invalid_validator_err() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
let validator = JwtValidator::no_check()
|
||||
.expiration_check_required()
|
||||
.not_before_check_optional();
|
||||
let err = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key)
|
||||
.unwrap()
|
||||
.validate::<MyClaims>(&validator)
|
||||
.err()
|
||||
.unwrap();
|
||||
assert_eq!(err.to_string(), "invalid validator: current date is missing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_jws_required_claim_missing_err() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
let now = JwtDate::new(0);
|
||||
let validator = JwtValidator::strict(now);
|
||||
let err = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key)
|
||||
.unwrap()
|
||||
.validate::<MyClaims>(&validator)
|
||||
.err()
|
||||
.unwrap();
|
||||
assert_eq!(err.to_string(), "required claim `nbf` is missing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_jws_rsa_sha256_using_json_value_claims() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
let validator = JwtValidator::no_check();
|
||||
let jwt = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key)
|
||||
.unwrap()
|
||||
.validate::<serde_json::Value>(&validator)
|
||||
.unwrap();
|
||||
assert_eq!(jwt.state.claims["sub"].as_str().expect("sub"), "1234567890");
|
||||
assert_eq!(jwt.state.claims["name"].as_str().expect("name"), "John Doe");
|
||||
assert!(jwt.state.claims["admin"].as_bool().expect("sub"));
|
||||
assert_eq!(jwt.state.claims["iat"].as_i64().expect("iat"), 1516239022);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jwe_direct_aes_256_gcm() {
|
||||
let claims = get_strongly_typed_claims();
|
||||
let key = crate::hash::HashAlgorithm::SHA2_256.digest(b"magic_password");
|
||||
let jwt = CheckedJwtEnc::new(JweAlg::Direct, JweEnc::Aes256Gcm, claims);
|
||||
let encoded = jwt.encode_direct(&key).unwrap();
|
||||
let decoded = JwtEnc::decode_direct(&encoded, &key)
|
||||
.unwrap()
|
||||
.validate::<MyClaims>(&NO_CHECK_VALIDATOR)
|
||||
.unwrap();
|
||||
assert_eq!(decoded.state.claims, get_strongly_typed_claims());
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MyExpirableClaims {
|
||||
exp: i64,
|
||||
nbf: i64,
|
||||
msg: String,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_jws_not_expired() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
|
||||
let jwt = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_WITH_EXP, &public_key)
|
||||
.unwrap()
|
||||
.validate::<MyExpirableClaims>(&JwtValidator::strict(JwtDate::new(1545263999)))
|
||||
.expect("couldn't decode jwt without leeway");
|
||||
|
||||
assert_eq!(jwt.state.claims.exp, 1545264000);
|
||||
assert_eq!(jwt.state.claims.nbf, 1545263000);
|
||||
assert_eq!(jwt.state.claims.msg, "THIS IS TIME SENSITIVE DATA");
|
||||
|
||||
// alternatively, a leeway can account for small clock skew
|
||||
JwtSig::decode(picky_test_data::JOSE_JWT_SIG_WITH_EXP, &public_key)
|
||||
.unwrap()
|
||||
.validate::<MyExpirableClaims>(&JwtValidator::strict(JwtDate::new_with_leeway(1545264001, 10)))
|
||||
.expect("couldn't decode jwt with leeway for exp");
|
||||
|
||||
JwtSig::decode(picky_test_data::JOSE_JWT_SIG_WITH_EXP, &public_key)
|
||||
.unwrap()
|
||||
.validate::<MyExpirableClaims>(&JwtValidator::strict(JwtDate::new_with_leeway(1545262999, 10)))
|
||||
.expect("couldn't decode jwt with leeway for nbf");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_jws_invalid_date_err() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
|
||||
let err = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_WITH_EXP, &public_key)
|
||||
.unwrap()
|
||||
.validate::<MyExpirableClaims>(&JwtValidator::strict(JwtDate::new(1545264001)))
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"token expired (not after: 1545264000, now: 1545264001 [leeway: 0])"
|
||||
);
|
||||
|
||||
let err = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_WITH_EXP, &public_key)
|
||||
.unwrap()
|
||||
.validate::<MyExpirableClaims>(&JwtValidator::strict(JwtDate::new_with_leeway(1545262998, 1)))
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"token not yet valid (not before: 1545263000, now: 1545262998 [leeway: 1])"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_step_cli_generated_token() {
|
||||
#[derive(Deserialize)]
|
||||
struct SomeJetClaims {
|
||||
jet_ap: String,
|
||||
prx_usr: String,
|
||||
nbf: i64,
|
||||
}
|
||||
|
||||
let jws = RawJws::decode("eyJhbGciOiJSUzI1NiIsImtpZCI6InUzQkF1b3lrZ21FY0F2Z21ydm5PVWxNZUYxN2JjS09EbGYweFdHcDhMY2MiLCJ0eXAiOiJKV1QifQ.eyJpYXQiOjE2NTkxMTg1NjMsImpldF9hcCI6InJkcCIsImp0aSI6IjY1YjkwZmQwMjM2YWU3Mjg1OWE1YWZlZTM3MTEzOTdjOWU4NTI1YzA4YzIyNjE4N2NlNjJjOWQwNTEzNDUzOTUiLCJuYmYiOjE2NTkxMTg1NjMsInByeF91c3IiOiJ1c2VybmFtZSJ9.MzULmkNyVY48nOgN7zbtN9q8Ni8JRavpkbw34aD-lMfqJzl5pFEJQPV9G1iM1HCbcMPRJfMDjVP31dAHOVtsu-gqGRx9qw1ogpNffcJI0nh5-VPPnqBbT5u8H2rJ7WeXO5kx4KAnD2Fbc45Nb6YEM-f_s9RyFipub0LI5AwiUHcbicJno0Lxz0dFKMiSA4cTNOe22vY7STf-E52LnsdHhnTt3JKDPP-7i5FzL1wOdBHzvxhRpyLqNU1kcSXrV_1L07XekeR6Kp3JoWaaJsIWm1Sk27W13Q575gS0a9OJgGX0bumq9fCneOJgLU8HrelUP8-qRM2IaGV81NRAr5HasQ")
|
||||
.map(RawJws::discard_signature)
|
||||
.map(JwtSig::from)
|
||||
.unwrap()
|
||||
.validate::<SomeJetClaims>(&JwtValidator::no_check())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(jws.state.claims.jet_ap, "rdp");
|
||||
assert_eq!(jws.state.claims.prx_usr, "username");
|
||||
assert_eq!(jws.state.claims.nbf, 1659118563);
|
||||
}
|
||||
}
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
//! JOSE framework subset implementation.
|
||||
//!
|
||||
//! A Json Web Token (JWT) comes in two flavors, roughly:
|
||||
//! - Json Web Encryption (JWE), used to transfer data securely
|
||||
//! - Json Web Signature (JWS), used to assert one's identity
|
||||
//!
|
||||
//! Common part is known as the "JOSE header".
|
||||
//!
|
||||
//! JSON Web Key (JWK) are used to represent cryptographic keys using JSON.
|
||||
|
||||
pub mod jwe;
|
||||
pub mod jwk;
|
||||
pub mod jws;
|
||||
pub mod jwt;
|
||||
Vendored
+396
@@ -0,0 +1,396 @@
|
||||
use crate::key::{KeyError, PrivateKey, PrivateKeyKind, PublicKey};
|
||||
use crate::oid::ObjectIdentifier;
|
||||
|
||||
use picky_asn1::wrapper::BitStringAsn1;
|
||||
use picky_asn1_x509::{EcParameters, oids};
|
||||
use std::fmt::Display;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct EcdsaKeypair {
|
||||
curve: NamedEcCurve,
|
||||
private_key: Vec<u8>,
|
||||
public_key: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl EcdsaKeypair {
|
||||
pub fn curve(&self) -> &NamedEcCurve {
|
||||
&self.curve
|
||||
}
|
||||
|
||||
pub fn secret(&self) -> &[u8] {
|
||||
&self.private_key
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EcdsaKeypair {
|
||||
fn drop(&mut self) {
|
||||
self.private_key.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum EcComponent<'a> {
|
||||
PointX(&'a [u8]),
|
||||
PointY(&'a [u8]),
|
||||
Secret(&'a [u8]),
|
||||
}
|
||||
|
||||
/// Elliptic curve name to use for curve operations which require curve-specific arithmetic.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum EcCurve {
|
||||
/// NIST P-256 curve (secp256r1)
|
||||
NistP256,
|
||||
/// NIST P-384 curve (secp384r1)
|
||||
NistP384,
|
||||
/// NIST P-521 curve (secp521r1)
|
||||
NistP521,
|
||||
}
|
||||
|
||||
impl EcCurve {
|
||||
/// Get size of field compoennet in bytes (e.g. X and Y point values, Secret key,
|
||||
/// R and S signature values)
|
||||
pub(crate) fn field_bytes_size(self) -> usize {
|
||||
match self {
|
||||
EcCurve::NistP256 => {
|
||||
use p256::elliptic_curve::FieldBytesSize;
|
||||
use p256::elliptic_curve::array::typenum::Unsigned;
|
||||
<FieldBytesSize<p256::NistP256> as Unsigned>::USIZE
|
||||
}
|
||||
EcCurve::NistP384 => {
|
||||
use p384::elliptic_curve::FieldBytesSize;
|
||||
use p384::elliptic_curve::array::typenum::Unsigned;
|
||||
<FieldBytesSize<p384::NistP384> as Unsigned>::USIZE
|
||||
}
|
||||
EcCurve::NistP521 => {
|
||||
use p521::elliptic_curve::FieldBytesSize;
|
||||
use p521::elliptic_curve::array::typenum::Unsigned;
|
||||
<FieldBytesSize<p521::NistP521> as Unsigned>::USIZE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// We need to validate input data sizes to prevent panics in the underlying `generic_array`
|
||||
/// library code.
|
||||
pub(crate) fn validate_component<'a>(&self, component: EcComponent<'a>) -> Result<&'a [u8], KeyError> {
|
||||
let (buffer, error_message) = match component {
|
||||
EcComponent::PointX(buf) => (buf, "Invalid `point.x` component size"),
|
||||
EcComponent::PointY(buf) => (buf, "Invalid `point.y` component size"),
|
||||
EcComponent::Secret(buf) => (buf, "Invalid `secret` component size"),
|
||||
};
|
||||
|
||||
if buffer.len() != self.field_bytes_size() {
|
||||
return Err(KeyError::EC {
|
||||
context: error_message.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes the curve type of an ECDSA keypair
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum NamedEcCurve {
|
||||
Known(EcCurve),
|
||||
Unsupported(ObjectIdentifier),
|
||||
}
|
||||
|
||||
impl Display for EcCurve {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NistP256 => write!(f, "NIST-P256"),
|
||||
Self::NistP384 => write!(f, "NIST-P384"),
|
||||
Self::NistP521 => write!(f, "NIST-P521"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for NamedEcCurve {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Known(curve) => curve.fmt(f),
|
||||
Self::Unsupported(oid) => {
|
||||
let oid: String = oid.into();
|
||||
write!(f, "Unsupported(OID: {oid})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'_ ObjectIdentifier> for NamedEcCurve {
|
||||
fn from(value: &ObjectIdentifier) -> Self {
|
||||
let oid: String = value.into();
|
||||
match oid.as_str() {
|
||||
oids::SECP256R1 => NamedEcCurve::Known(EcCurve::NistP256),
|
||||
oids::SECP384R1 => NamedEcCurve::Known(EcCurve::NistP384),
|
||||
oids::SECP521R1 => NamedEcCurve::Known(EcCurve::NistP521),
|
||||
_ => NamedEcCurve::Unsupported(value.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NamedEcCurve> for ObjectIdentifier {
|
||||
fn from(value: NamedEcCurve) -> Self {
|
||||
match value {
|
||||
NamedEcCurve::Known(curve) => match curve {
|
||||
EcCurve::NistP256 => oids::secp256r1(),
|
||||
EcCurve::NistP384 => oids::secp384r1(),
|
||||
EcCurve::NistP521 => oids::secp521r1(),
|
||||
},
|
||||
NamedEcCurve::Unsupported(oid) => oid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a PrivateKey> for EcdsaKeypair {
|
||||
type Error = KeyError;
|
||||
|
||||
fn try_from(v: &'a PrivateKey) -> Result<Self, Self::Error> {
|
||||
match &v.kind {
|
||||
PrivateKeyKind::Ec {
|
||||
public_key,
|
||||
private_key,
|
||||
curve_oid,
|
||||
..
|
||||
} => Ok(Self {
|
||||
curve: NamedEcCurve::from(curve_oid),
|
||||
private_key: private_key.clone(),
|
||||
public_key: public_key.clone(),
|
||||
}),
|
||||
_ => Err(KeyError::EC {
|
||||
context: "EC keypair cannot be built from Non-EC private key".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn calculate_public_ec_key(
|
||||
curve_oid: &ObjectIdentifier,
|
||||
private_key: &[u8],
|
||||
compress: bool,
|
||||
) -> Result<Option<Vec<u8>>, KeyError> {
|
||||
let curve = NamedEcCurve::from(curve_oid);
|
||||
|
||||
match curve {
|
||||
NamedEcCurve::Known(EcCurve::NistP256) => {
|
||||
use p256::elliptic_curve::sec1::ToSec1Point as _;
|
||||
|
||||
let private_key_validated = EcCurve::NistP256.validate_component(EcComponent::Secret(private_key))?;
|
||||
|
||||
let secret_bytes =
|
||||
p256::elliptic_curve::array::Array::try_from(private_key_validated).map_err(|_| KeyError::EC {
|
||||
context: format!(
|
||||
"validated private key is the not right length(expected: {}, actual: {})",
|
||||
EcCurve::NistP256.field_bytes_size(),
|
||||
private_key_validated.len()
|
||||
),
|
||||
})?;
|
||||
let secret_key = p256::SecretKey::from_bytes(&secret_bytes).map_err(|_| KeyError::EC {
|
||||
context: "Failed to construct P256 SecretKey from private key bytes".to_string(),
|
||||
})?;
|
||||
|
||||
// Calculate public key from secret key
|
||||
let public_key = secret_key.public_key().as_affine().to_sec1_point(compress);
|
||||
|
||||
Ok(Some(public_key.to_bytes().to_vec()))
|
||||
}
|
||||
NamedEcCurve::Known(EcCurve::NistP384) => {
|
||||
use p384::elliptic_curve::sec1::ToSec1Point as _;
|
||||
|
||||
let private_key_validated = EcCurve::NistP384.validate_component(EcComponent::Secret(private_key))?;
|
||||
|
||||
let secret_bytes =
|
||||
p384::elliptic_curve::array::Array::try_from(private_key_validated).map_err(|_| KeyError::EC {
|
||||
context: format!(
|
||||
"validated private key is the not right length(expected: {}, actual: {})",
|
||||
EcCurve::NistP384.field_bytes_size(),
|
||||
private_key_validated.len()
|
||||
),
|
||||
})?;
|
||||
let secret_key = p384::SecretKey::from_bytes(&secret_bytes).map_err(|_| KeyError::EC {
|
||||
context: "Failed to construct P384 SecretKey from private key bytes".to_string(),
|
||||
})?;
|
||||
|
||||
// Calculate public key from secret key
|
||||
let public_key = secret_key.public_key().as_affine().to_sec1_point(compress);
|
||||
|
||||
Ok(Some(public_key.to_bytes().to_vec()))
|
||||
}
|
||||
NamedEcCurve::Known(EcCurve::NistP521) => {
|
||||
use p521::elliptic_curve::sec1::ToSec1Point as _;
|
||||
|
||||
let private_key_validated = EcCurve::NistP521.validate_component(EcComponent::Secret(private_key))?;
|
||||
|
||||
let secret_bytes =
|
||||
p521::elliptic_curve::array::Array::try_from(private_key_validated).map_err(|_| KeyError::EC {
|
||||
context: format!(
|
||||
"validated private key is the not right length(expected: {}, actual: {})",
|
||||
EcCurve::NistP521.field_bytes_size(),
|
||||
private_key_validated.len()
|
||||
),
|
||||
})?;
|
||||
let secret_key = p521::SecretKey::from_bytes(&secret_bytes).map_err(|_| KeyError::EC {
|
||||
context: "Failed to construct P521 SecretKey from private key bytes".to_string(),
|
||||
})?;
|
||||
|
||||
// Calculate public key from secret key
|
||||
let public_key = secret_key.public_key().as_affine().to_sec1_point(compress);
|
||||
|
||||
Ok(Some(public_key.to_bytes().to_vec()))
|
||||
}
|
||||
NamedEcCurve::Unsupported(_) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct EcdsaPublicKey<'a> {
|
||||
data: &'a [u8],
|
||||
curve: NamedEcCurve,
|
||||
}
|
||||
|
||||
impl EcdsaPublicKey<'_> {
|
||||
pub fn curve(&self) -> &NamedEcCurve {
|
||||
&self.curve
|
||||
}
|
||||
|
||||
pub fn encoded_point(&self) -> &[u8] {
|
||||
self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a PublicKey> for EcdsaPublicKey<'a> {
|
||||
type Error = KeyError;
|
||||
|
||||
fn try_from(v: &'a PublicKey) -> Result<Self, Self::Error> {
|
||||
use picky_asn1_x509::PublicKey as InnerPublicKey;
|
||||
|
||||
let curve_oid = match &v.as_inner().algorithm.parameters() {
|
||||
picky_asn1_x509::AlgorithmIdentifierParameters::Ec(EcParameters::NamedCurve(curve_oid)) => {
|
||||
curve_oid.0.clone()
|
||||
}
|
||||
_ => {
|
||||
return Err(KeyError::EC {
|
||||
context: "EC public key cannot be constructed from non-EC public key".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
match &v.as_inner().subject_public_key {
|
||||
InnerPublicKey::Rsa(_) => Err(KeyError::EC {
|
||||
context: "EC public key cannot be constructed from RSA public key".to_string(),
|
||||
}),
|
||||
InnerPublicKey::Ec(BitStringAsn1(bitstring)) => {
|
||||
let data = bitstring.payload_view();
|
||||
|
||||
Ok(EcdsaPublicKey {
|
||||
data,
|
||||
curve: NamedEcCurve::from(&curve_oid),
|
||||
})
|
||||
}
|
||||
InnerPublicKey::Ed(_) => Err(KeyError::EC {
|
||||
context: "EC public key cannot be constructed from ED25519 public key".to_string(),
|
||||
}),
|
||||
InnerPublicKey::Mldsa(_) => Err(KeyError::EC {
|
||||
context: "EC public key cannot be constructed from MLDSA public key".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a EcdsaKeypair> for EcdsaPublicKey<'a> {
|
||||
type Error = KeyError;
|
||||
|
||||
fn try_from(v: &'a EcdsaKeypair) -> Result<Self, Self::Error> {
|
||||
match v.public_key.as_ref() {
|
||||
Some(key) => Ok(Self {
|
||||
data: key.as_slice(),
|
||||
curve: v.curve.clone(),
|
||||
}),
|
||||
None => Err(KeyError::EC {
|
||||
context: "EC public key cannot be constructed from EC private key without public key".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rstest::*;
|
||||
|
||||
const RSA_PUBLIC_KEY_PEM: &str = "-----BEGIN RSA PUBLIC KEY-----\n\
|
||||
MIIBCgKCAQEA61BjmfXGEvWmegnBGSuS+rU9soUg2FnODva32D1AqhwdziwHINFa\n\
|
||||
D1MVlcrYG6XRKfkcxnaXGfFDWHLEvNBSEVCgJjtHAGZIm5GL/KA86KDp/CwDFMSw\n\
|
||||
luowcXwDwoyinmeOY9eKyh6aY72xJh7noLBBq1N0bWi1e2i+83txOCg4yV2oVXhB\n\
|
||||
o8pYEJ8LT3el6Smxol3C1oFMVdwPgc0vTl25XucMcG/ALE/KNY6pqC2AQ6R2ERlV\n\
|
||||
gPiUWOPatVkt7+Bs3h5Ramxh7XjBOXeulmCpGSynXNcpZ/06+vofGi/2MlpQZNhH\n\
|
||||
Ao8eayMp6FcvNucIpUndo1X8dKMv3Y26ZQIDAQAB\n\
|
||||
-----END RSA PUBLIC KEY-----";
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::EC_NIST256_DER_PK_1)]
|
||||
#[case(picky_test_data::EC_NIST384_DER_PK_1)]
|
||||
#[case(picky_test_data::EC_NIST521_DER_PK_1)]
|
||||
#[case(picky_test_data::EC_NIST256_PK_1)] // PKCS8
|
||||
fn private_key_from_ec_pem(#[case] key_pem: &str) {
|
||||
PrivateKey::from_pem_str(key_pem).unwrap();
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::EC_NIST256_NOPUBLIC_DER_PK_1)]
|
||||
#[case(picky_test_data::EC_NIST384_NOPUBLIC_DER_PK_1)]
|
||||
#[case(picky_test_data::EC_NIST521_NOPUBLIC_DER_PK_1)]
|
||||
fn ecdsa_private_key_without_public(#[case] key_pem: &str) {
|
||||
// This should succeed for supported curves
|
||||
let key = PrivateKey::from_pem_str(key_pem).unwrap();
|
||||
key.to_public_key().unwrap().to_pem_str().unwrap();
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
// Known curves
|
||||
#[case(picky_test_data::EC_NIST256_PK_1_PUB)]
|
||||
#[case(picky_test_data::EC_NIST384_PK_1_PUB)]
|
||||
#[case(picky_test_data::EC_NIST521_PK_1_PUB)]
|
||||
// Unsupported curve, should still work as long as pem contains the public key
|
||||
// (in that case no arithmetic operations are performed on the key)
|
||||
#[case(picky_test_data::EC_PUBLIC_KEY_SECP256K1_PEM)]
|
||||
fn ecdsa_public_valid_key_conversions(#[case] key_pem: &str) {
|
||||
let pk: &PublicKey = &PublicKey::from_pem_str(key_pem).unwrap();
|
||||
let epk: Result<EcdsaPublicKey, KeyError> = pk.try_into();
|
||||
assert!(epk.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecdsa_public_invalid_key_conversions() {
|
||||
// PEM public key conversion fails with an error
|
||||
let pk: &PublicKey = &PublicKey::from_pem_str(RSA_PUBLIC_KEY_PEM).unwrap();
|
||||
let epk: Result<EcdsaPublicKey, KeyError> = pk.try_into();
|
||||
assert!(epk.is_err());
|
||||
assert!(matches!(epk, Err(KeyError::EC { context: _ })));
|
||||
|
||||
// TODO: add check for attempted conversion from ED keys - which are not supported yet
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::EC_NIST256_DER_PK_1, NamedEcCurve::Known(EcCurve::NistP256))]
|
||||
#[case(picky_test_data::EC_NIST384_DER_PK_1, NamedEcCurve::Known(EcCurve::NistP384))]
|
||||
#[case(picky_test_data::EC_NIST521_DER_PK_1, NamedEcCurve::Known(EcCurve::NistP521))]
|
||||
fn ecdsa_key_pair_from_ec_private_key(#[case] key: &str, #[case] curve: NamedEcCurve) {
|
||||
let pk = PrivateKey::from_pem_str(key).unwrap();
|
||||
let pair = EcdsaKeypair::try_from(&pk).unwrap();
|
||||
assert_eq!(curve, pair.curve);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ring_ecdsa_pkcs8_keys_could_be_parsed() {
|
||||
let algo = &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING;
|
||||
let rng = ring::rand::SystemRandom::new();
|
||||
let pkcs8_bytes = ring::signature::EcdsaKeyPair::generate_pkcs8(algo, &rng).unwrap();
|
||||
// Validate that missing `parameters` field from ECPriavteKeyInfo is handled correctly.
|
||||
// rings skips it during pkcs8 serialization
|
||||
let key = PrivateKey::from_pkcs8(&pkcs8_bytes).unwrap();
|
||||
let pair = EcdsaKeypair::try_from(&key).unwrap();
|
||||
|
||||
assert_eq!(pair.curve(), &NamedEcCurve::Known(EcCurve::NistP256))
|
||||
}
|
||||
}
|
||||
Vendored
+240
@@ -0,0 +1,240 @@
|
||||
use crate::key::{KeyError, PrivateKey, PrivateKeyKind, PublicKey};
|
||||
use crate::oid::ObjectIdentifier;
|
||||
|
||||
use picky_asn1::wrapper::BitStringAsn1;
|
||||
use picky_asn1_x509::oids;
|
||||
use std::fmt::Display;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
pub(crate) const X25519_FIELD_ELEMENT_SIZE: usize = 32;
|
||||
pub(crate) type X25519FieldElement = [u8; X25519_FIELD_ELEMENT_SIZE];
|
||||
|
||||
/// Name of supported Curve25519 and Curve448 based algorithms.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum EdAlgorithm {
|
||||
/// Curve25519-based EdDSA algorithm
|
||||
Ed25519,
|
||||
/// Curve25519-based ECDH algorithm (mainly used for jwe key agreement)
|
||||
X25519,
|
||||
// (Unsupported) Ed448 -- Curve448-based EdDSA algorithm
|
||||
// (Unsupported) X448 -- Curve448-based ECDH algorithm (mainly used for jwe key agreement)
|
||||
}
|
||||
|
||||
// Describes Edwards curve-based EC algorithm
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum NamedEdAlgorithm {
|
||||
Known(EdAlgorithm),
|
||||
Unsupported(ObjectIdentifier),
|
||||
}
|
||||
|
||||
impl Display for NamedEdAlgorithm {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
NamedEdAlgorithm::Known(alg) => write!(f, "{alg}"),
|
||||
NamedEdAlgorithm::Unsupported(oid) => {
|
||||
// We don't support Ed448 and X448 algorithms, but we can still print their named
|
||||
// representation of OID to make errrs more readable.
|
||||
if oid == &oids::ed448() {
|
||||
write!(f, "Ed448")
|
||||
} else if oid == &oids::x448() {
|
||||
write!(f, "X448")
|
||||
} else {
|
||||
let oid: String = oid.into();
|
||||
write!(f, "Unsupported(OID: {oid})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for EdAlgorithm {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Ed25519 => write!(f, "Ed25519"),
|
||||
Self::X25519 => write!(f, "X25519"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'_ ObjectIdentifier> for NamedEdAlgorithm {
|
||||
fn from(value: &'_ ObjectIdentifier) -> Self {
|
||||
let oid: String = value.into();
|
||||
match oid.as_str() {
|
||||
oids::ED25519 => NamedEdAlgorithm::Known(EdAlgorithm::Ed25519),
|
||||
oids::X25519 => NamedEdAlgorithm::Known(EdAlgorithm::X25519),
|
||||
_ => NamedEdAlgorithm::Unsupported(value.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EdAlgorithm> for ObjectIdentifier {
|
||||
fn from(value: EdAlgorithm) -> Self {
|
||||
match value {
|
||||
EdAlgorithm::Ed25519 => oids::ed25519(),
|
||||
EdAlgorithm::X25519 => oids::x25519(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NamedEdAlgorithm> for ObjectIdentifier {
|
||||
fn from(value: NamedEdAlgorithm) -> Self {
|
||||
match value {
|
||||
NamedEdAlgorithm::Known(alg) => alg.into(),
|
||||
NamedEdAlgorithm::Unsupported(oid) => oid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct EdKeypair {
|
||||
algorithm: NamedEdAlgorithm,
|
||||
private_key: Vec<u8>,
|
||||
public_key: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl EdKeypair {
|
||||
pub fn algorithm(&self) -> &NamedEdAlgorithm {
|
||||
&self.algorithm
|
||||
}
|
||||
|
||||
pub fn secret(&self) -> &[u8] {
|
||||
&self.private_key
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EdKeypair {
|
||||
fn drop(&mut self) {
|
||||
self.private_key.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a PrivateKey> for EdKeypair {
|
||||
type Error = KeyError;
|
||||
|
||||
fn try_from(value: &'a PrivateKey) -> Result<Self, Self::Error> {
|
||||
match &value.kind {
|
||||
PrivateKeyKind::Ed {
|
||||
public_key,
|
||||
private_key,
|
||||
algorithm_oid,
|
||||
} => Ok(Self {
|
||||
algorithm: NamedEdAlgorithm::from(algorithm_oid),
|
||||
private_key: private_key.clone(),
|
||||
public_key: public_key.clone(),
|
||||
}),
|
||||
_ => Err(KeyError::ED {
|
||||
context: "Ed keypair cannot be constructed from non-Ed private key".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct EdPublicKey<'a> {
|
||||
data: &'a [u8],
|
||||
algorithm: NamedEdAlgorithm,
|
||||
}
|
||||
|
||||
impl EdPublicKey<'_> {
|
||||
pub fn algorithm(&self) -> &NamedEdAlgorithm {
|
||||
&self.algorithm
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &[u8] {
|
||||
self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a EdKeypair> for EdPublicKey<'a> {
|
||||
type Error = KeyError;
|
||||
|
||||
fn try_from(v: &'a EdKeypair) -> Result<Self, Self::Error> {
|
||||
match v.public_key.as_ref() {
|
||||
Some(key) => Ok(Self {
|
||||
data: key.as_slice(),
|
||||
algorithm: v.algorithm.clone(),
|
||||
}),
|
||||
None => Err(KeyError::ED {
|
||||
context: "Ed public key cannot be constructed from Ed private key without public key".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a PublicKey> for EdPublicKey<'a> {
|
||||
type Error = KeyError;
|
||||
|
||||
fn try_from(v: &'a PublicKey) -> Result<Self, Self::Error> {
|
||||
use picky_asn1_x509::PublicKey as InnerPublicKey;
|
||||
|
||||
let oid = v.as_inner().algorithm.oid();
|
||||
|
||||
match &v.as_inner().subject_public_key {
|
||||
InnerPublicKey::Rsa(_) => Err(KeyError::ED {
|
||||
context: "Ed public key cannot be constructed from RSA public key".to_string(),
|
||||
}),
|
||||
InnerPublicKey::Ec(_) => Err(KeyError::ED {
|
||||
context: "Ed public key cannot be constructed from Ec public key".to_string(),
|
||||
}),
|
||||
InnerPublicKey::Ed(BitStringAsn1(bitstring)) => {
|
||||
let data = bitstring.payload_view();
|
||||
|
||||
Ok(EdPublicKey {
|
||||
data,
|
||||
algorithm: NamedEdAlgorithm::from(oid),
|
||||
})
|
||||
}
|
||||
InnerPublicKey::Mldsa(_) => Err(KeyError::ED {
|
||||
context: "Ed public key cannot be constructed from Mldsa public key".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::key::{PrivateKey, PublicKey};
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::ED25519_PEM_PK_1)]
|
||||
#[case(picky_test_data::X25519_PEM_PK_1)]
|
||||
// Although X448 and ED448 are not supported, we should still be able to decode and encode them
|
||||
#[case(picky_test_data::ED448_PEM_PK_1)]
|
||||
#[case(picky_test_data::X448_PEM_PK_1)]
|
||||
fn private_key_roundtrip(#[case] key_pem: &str) {
|
||||
let decoded = PrivateKey::from_pem_str(key_pem).unwrap();
|
||||
let encoded = decoded.to_pem_str().unwrap();
|
||||
assert_eq!(encoded.as_str(), key_pem);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::ED25519_PEM_PK_1_PUB)]
|
||||
#[case(picky_test_data::X25519_PEM_PK_1_PUB)]
|
||||
// Although X448 and ED448 are not supported, we should still be able to decode and encode them
|
||||
#[case(picky_test_data::ED448_PEM_PK_1_PUB)]
|
||||
#[case(picky_test_data::X448_PEM_PK_1_PUB)]
|
||||
fn public_key_roundtrip(#[case] key_pem: &str) {
|
||||
let decoded = PublicKey::from_pem_str(key_pem).unwrap();
|
||||
let encoded = decoded.to_pem_str().unwrap();
|
||||
assert_eq!(encoded.as_str(), key_pem);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::ED25519_PEM_PK_1, picky_test_data::ED25519_PEM_PK_1_PUB)]
|
||||
#[case(picky_test_data::X25519_PEM_PK_1, picky_test_data::X25519_PEM_PK_1_PUB)]
|
||||
fn extract_public_key(#[case] key_pem: &str, #[case] expected_public_pem: &str) {
|
||||
let private = PrivateKey::from_pem_str(key_pem).unwrap();
|
||||
let public = private.to_public_key().unwrap();
|
||||
let public_expected = PublicKey::from_pem_str(expected_public_pem).unwrap();
|
||||
assert_eq!(public, public_expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::ED448_PEM_PK_1)]
|
||||
#[case(picky_test_data::X448_PEM_PK_1)]
|
||||
fn extract_public_key_for_unsupported_algorithm_fails(#[case] key_pem: &str) {
|
||||
let private = PrivateKey::from_pem_str(key_pem).unwrap();
|
||||
assert!(private.to_public_key().is_err());
|
||||
}
|
||||
}
|
||||
Vendored
+1299
File diff suppressed because it is too large
Load Diff
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
//! [](https://crates.io/crates/picky)
|
||||
//! [](https://docs.rs/picky)
|
||||
//! 
|
||||
//! # picky
|
||||
//!
|
||||
//! Portable X.509, PKI, JOSE and HTTP signature implementation.
|
||||
|
||||
#[cfg(feature = "http_signature")]
|
||||
pub mod http;
|
||||
|
||||
#[cfg(feature = "jose")]
|
||||
pub mod jose;
|
||||
|
||||
#[cfg(feature = "x509")]
|
||||
pub mod x509;
|
||||
|
||||
#[cfg(feature = "ssh")]
|
||||
pub mod ssh;
|
||||
|
||||
#[cfg(feature = "pkcs12")]
|
||||
pub mod pkcs12;
|
||||
|
||||
#[cfg(feature = "putty")]
|
||||
pub mod putty;
|
||||
|
||||
pub mod hash;
|
||||
pub mod key;
|
||||
pub mod pem;
|
||||
pub mod signature;
|
||||
|
||||
pub use picky_asn1_x509::{AlgorithmIdentifier, oid, oids};
|
||||
Vendored
+292
@@ -0,0 +1,292 @@
|
||||
//! Privacy-Enhanced Mail (PEM) format utilities
|
||||
//!
|
||||
//! Based on the RFC-7468
|
||||
//! ([Textual Encodings of PKIX, PKCS, and CMS Structures](https://tools.ietf.org/html/rfc7468)).
|
||||
|
||||
use base64::engine::general_purpose;
|
||||
use base64::{DecodeError, Engine as _};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::io::BufRead;
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
|
||||
const PEM_HEADER_START: &str = "-----BEGIN";
|
||||
const PEM_FOOTER_START: &str = "-----END";
|
||||
const PEM_DASHES_BOUNDARIES: &str = "-----";
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum PemError {
|
||||
/// header not found
|
||||
#[error("header not found")]
|
||||
HeaderNotFound,
|
||||
|
||||
/// invalid pem header
|
||||
#[error("invalid pem header")]
|
||||
InvalidHeader,
|
||||
|
||||
/// footer not found
|
||||
#[error("footer not found")]
|
||||
FooterNotFound,
|
||||
|
||||
/// couldn't decode base64
|
||||
#[error("couldn't decode base64: {source}")]
|
||||
Base64Decoding { source: DecodeError },
|
||||
}
|
||||
|
||||
/// Privacy-Enhanced Mail (PEM) format structured representation
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Pem<'a> {
|
||||
label: String,
|
||||
data: Cow<'a, [u8]>,
|
||||
}
|
||||
|
||||
impl<'a> Pem<'a> {
|
||||
pub fn new<S: Into<String>, D: Into<Cow<'a, [u8]>>>(label: S, data: D) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
data: data.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(&self) -> &str {
|
||||
&self.label
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn into_data(self) -> Cow<'a, [u8]> {
|
||||
self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl Pem<'static> {
|
||||
pub fn read_from(reader: &mut impl BufRead) -> Result<Self, PemError> {
|
||||
read_pem(reader)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Pem<'static> {
|
||||
type Err = PemError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
parse_pem(s.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Pem<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "{} {}-----", PEM_HEADER_START, self.label)?;
|
||||
|
||||
let encoded = general_purpose::STANDARD.encode(&self.data);
|
||||
let bytes = encoded.as_bytes();
|
||||
for chunk in bytes.chunks(64) {
|
||||
let chunk = std::str::from_utf8(chunk).map_err(|_| fmt::Error)?;
|
||||
writeln!(f, "{chunk}")?;
|
||||
}
|
||||
|
||||
write!(f, "{} {}-----", PEM_FOOTER_START, self.label)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pem<'_>> for String {
|
||||
fn from(pem: Pem<'_>) -> Self {
|
||||
pem.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a PEM-encoded stream from a [u8] representation
|
||||
///
|
||||
/// If the input contains line ending characters (`\r`, `\n`), a copy of input
|
||||
/// is allocated striping these. If you can strip these with minimal data copy
|
||||
/// you should do it beforehand.
|
||||
pub fn parse_pem<T: ?Sized + AsRef<[u8]>>(input: &T) -> Result<Pem<'static>, PemError> {
|
||||
parse_pem_impl(input.as_ref())
|
||||
}
|
||||
|
||||
fn parse_pem_impl(input: &[u8]) -> Result<Pem<'static>, PemError> {
|
||||
let header_start_idx = h_find(input, PEM_HEADER_START.as_bytes()).ok_or(PemError::HeaderNotFound)?;
|
||||
|
||||
let label_start_idx = header_start_idx + PEM_HEADER_START.len();
|
||||
let label_end_idx = h_find(&input[label_start_idx..], b"-").ok_or(PemError::InvalidHeader)? + label_start_idx;
|
||||
let label = String::from_utf8_lossy(&input[label_start_idx..label_end_idx])
|
||||
.trim()
|
||||
.to_owned();
|
||||
|
||||
let header_end_idx = h_find(&input[label_end_idx..], PEM_DASHES_BOUNDARIES.as_bytes())
|
||||
.ok_or(PemError::InvalidHeader)?
|
||||
+ label_end_idx
|
||||
+ PEM_DASHES_BOUNDARIES.len();
|
||||
|
||||
let footer_start_idx =
|
||||
h_find(&input[header_end_idx..], PEM_FOOTER_START.as_bytes()).ok_or(PemError::FooterNotFound)? + header_end_idx;
|
||||
|
||||
let raw_data = &input[header_end_idx..footer_start_idx];
|
||||
|
||||
let data = if h_find(raw_data, b"\n").is_some() {
|
||||
// Line ending characters should be striped... Sadly, this means we need to copy and allocate.
|
||||
let striped_raw_data: Vec<u8> = raw_data
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&byte| byte != b'\r' && byte != b'\n')
|
||||
.collect();
|
||||
general_purpose::STANDARD
|
||||
.decode(striped_raw_data)
|
||||
.map_err(|source| PemError::Base64Decoding { source })?
|
||||
} else {
|
||||
// Can be decoded as is!
|
||||
general_purpose::STANDARD
|
||||
.decode(raw_data)
|
||||
.map_err(|source| PemError::Base64Decoding { source })?
|
||||
};
|
||||
|
||||
Ok(Pem {
|
||||
label,
|
||||
data: Cow::Owned(data),
|
||||
})
|
||||
}
|
||||
|
||||
fn h_find(buffer: &[u8], value: &[u8]) -> Option<usize> {
|
||||
buffer.windows(value.len()).position(|window| window == value)
|
||||
}
|
||||
|
||||
/// Parse a PEM-encoded stream from a BufRead object.
|
||||
///
|
||||
/// Maybe slower than the AsRef<[u8]>-based implementation because additional copies are incurred,
|
||||
/// but in most cases it's probably easier to work with and not that bad anyway.
|
||||
pub fn read_pem(reader: &mut impl BufRead) -> Result<Pem<'static>, PemError> {
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
|
||||
// skip until start of header
|
||||
h_read_until(reader, PEM_HEADER_START.as_bytes(), &mut buf).ok_or(PemError::HeaderNotFound)?;
|
||||
buf.clear();
|
||||
|
||||
// read until end of header
|
||||
h_read_until(reader, PEM_DASHES_BOUNDARIES.as_bytes(), &mut buf).ok_or(PemError::InvalidHeader)?;
|
||||
let buf_utf8 = core::str::from_utf8(&buf).map_err(|_| PemError::InvalidHeader)?;
|
||||
let label = buf_utf8.trim_end_matches(PEM_DASHES_BOUNDARIES).trim().to_owned();
|
||||
buf.clear();
|
||||
|
||||
// read to footer
|
||||
h_read_until(reader, PEM_FOOTER_START.as_bytes(), &mut buf).ok_or(PemError::FooterNotFound)?;
|
||||
let base64_data: Vec<u8> = h_trim_end_matches(&buf, PEM_FOOTER_START.as_bytes())
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter(|&byte| byte != b'\r' && byte != b'\n')
|
||||
.collect();
|
||||
let data = general_purpose::STANDARD
|
||||
.decode(base64_data)
|
||||
.map_err(|source| PemError::Base64Decoding { source })?;
|
||||
|
||||
// read until end of footer
|
||||
h_read_until(reader, PEM_DASHES_BOUNDARIES.as_bytes(), &mut buf).ok_or(PemError::FooterNotFound)?;
|
||||
|
||||
Ok(Pem {
|
||||
label,
|
||||
data: Cow::Owned(data),
|
||||
})
|
||||
}
|
||||
|
||||
// Helper to read until some pattern is matched. Returns None on any error
|
||||
// (cannot be copy pasted for any purpose and should stay private!).
|
||||
fn h_read_until(reader: &mut impl BufRead, pat: &[u8], buf: &mut Vec<u8>) -> Option<usize> {
|
||||
let mut read = 0;
|
||||
let first_delim = *pat.first()?;
|
||||
'outer: loop {
|
||||
read += reader.read_until(first_delim, buf).ok()?;
|
||||
|
||||
for &next_delim in &pat[1..] {
|
||||
let mut next = [0];
|
||||
reader.read_exact(&mut next).ok()?;
|
||||
buf.push(next[0]);
|
||||
read += 1;
|
||||
|
||||
if next[0] != next_delim {
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
|
||||
break Some(read);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to trim trailing characters matching the given pattern for bytes slice
|
||||
fn h_trim_end_matches<'a>(slice: &'a [u8], pat: &[u8]) -> &'a [u8] {
|
||||
for (&slice_elem, &pat_elem) in slice.iter().rev().zip(pat.iter().rev()) {
|
||||
if slice_elem != pat_elem {
|
||||
return slice; // pattern doesn't match, return all the slice
|
||||
}
|
||||
}
|
||||
|
||||
// pattern did match, return sub-slice
|
||||
&slice[..slice.len() - pat.len()]
|
||||
}
|
||||
|
||||
/// Build a PEM-encoded structure into a String.
|
||||
pub fn to_pem<S, T>(label: S, data: &T) -> String
|
||||
where
|
||||
S: Into<String>,
|
||||
T: ?Sized + AsRef<[u8]>,
|
||||
{
|
||||
Pem::new(label, data.as_ref()).to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::BufReader;
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
let pem_from_bytes = parse_pem(picky_test_data::PEM_BYTES).unwrap();
|
||||
assert_eq!(pem_from_bytes.label, "CERTIFICATE");
|
||||
|
||||
let pem_from_str = picky_test_data::PEM_STR.parse::<Pem>().unwrap();
|
||||
pretty_assertions::assert_eq!(pem_from_bytes, pem_from_str);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reader_based() {
|
||||
let mut reader = BufReader::new(picky_test_data::PEM_BYTES);
|
||||
|
||||
let pem_from_reader = read_pem(&mut reader).unwrap();
|
||||
assert_eq!(pem_from_reader.label, "CERTIFICATE");
|
||||
|
||||
let pem_from_str = picky_test_data::PEM_STR.parse::<Pem>().unwrap();
|
||||
pretty_assertions::assert_eq!(pem_from_reader, pem_from_str);
|
||||
}
|
||||
|
||||
// This test should not run on Windows. writeln! add `/r` ending character to Pem in String format on Windows targets.
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn to_string() {
|
||||
let pem = picky_test_data::PEM_STR.parse::<Pem>().unwrap();
|
||||
let reconverted_pem = pem.to_string();
|
||||
pretty_assertions::assert_eq!(reconverted_pem, picky_test_data::PEM_STR);
|
||||
}
|
||||
|
||||
const FLATTENED_PEM: &str = "-----BEGIN GARBAGE-----R0FSQkFHRQo=-----END GARBAGE-----";
|
||||
|
||||
#[test]
|
||||
fn flattened() {
|
||||
FLATTENED_PEM.parse::<Pem>().unwrap();
|
||||
read_pem(&mut BufReader::new(FLATTENED_PEM.as_bytes())).unwrap();
|
||||
}
|
||||
|
||||
const MULTIPLE_PEM: &str = "-----BEGIN GARBAGE1-----R0FSQkFHRQo=-----END GARBAGE1-----\
|
||||
-----BEGIN GARBAGE2-----R0FSQkFHRQo=-----END GARBAGE2-----";
|
||||
|
||||
#[test]
|
||||
fn multiple() {
|
||||
// reading multiple PEM from some bytes stream is easier with read-based API
|
||||
let mut reader = BufReader::new(MULTIPLE_PEM.as_bytes());
|
||||
let pem1 = read_pem(&mut reader).unwrap();
|
||||
assert_eq!(pem1.label, "GARBAGE1");
|
||||
let pem2 = read_pem(&mut reader).unwrap();
|
||||
assert_eq!(pem2.label, "GARBAGE2");
|
||||
}
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
use crate::pkcs12::Pkcs12Error;
|
||||
use picky_asn1::restricted_string::BmpString;
|
||||
use picky_asn1::wrapper::OctetStringAsn1;
|
||||
use picky_asn1_der::Asn1RawDer;
|
||||
use picky_asn1_x509::oid::ObjectIdentifier;
|
||||
use picky_asn1_x509::pkcs12::Pkcs12Attribute as Pkcs12AttributeAsn1;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Represents a PKCS#12 attributes which can be used to store additional information about safe
|
||||
/// bag contents (e.g. private key or certificate).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Pkcs12Attribute {
|
||||
kind: Pkcs12AttributeKind,
|
||||
inner: Pkcs12AttributeAsn1,
|
||||
}
|
||||
|
||||
impl Pkcs12Attribute {
|
||||
pub(crate) fn from_asn1(asn1: Pkcs12AttributeAsn1) -> Self {
|
||||
let kind = match &asn1 {
|
||||
Pkcs12AttributeAsn1::FriendlyName(value) => Pkcs12AttributeKind::FriendlyName(value.clone()),
|
||||
Pkcs12AttributeAsn1::LocalKeyId(value) => Pkcs12AttributeKind::LocalKeyId(value.0.clone()),
|
||||
Pkcs12AttributeAsn1::Unknown { oid, value } => Pkcs12AttributeKind::Custom(CustomPkcs12Attribute {
|
||||
oid: oid.clone(),
|
||||
value: value.clone(),
|
||||
}),
|
||||
};
|
||||
|
||||
Self { kind, inner: asn1 }
|
||||
}
|
||||
|
||||
/// Creates a new `friendly name` attribute. This attribute is used to store a human-readable
|
||||
/// name of the safe bag contents (e.g. certificate name).
|
||||
pub fn new_friendly_name(value: BmpString) -> Self {
|
||||
let kind = Pkcs12AttributeKind::FriendlyName(value);
|
||||
let inner = kind.to_inner();
|
||||
Self { kind, inner }
|
||||
}
|
||||
|
||||
/// Creates a new `local key id` attribute. This attribute is used to indicate relation between
|
||||
/// private key and certificate (when set to same value on both objects).
|
||||
pub fn new_local_key_id(value: impl Into<Vec<u8>>) -> Self {
|
||||
let kind = Pkcs12AttributeKind::LocalKeyId(value.into());
|
||||
let inner = kind.to_inner();
|
||||
Self { kind, inner }
|
||||
}
|
||||
|
||||
/// Create a new custom attribute (e.g. Microsoft-specific attributes).
|
||||
pub fn new_custom(attr: CustomPkcs12Attribute) -> Self {
|
||||
let kind = Pkcs12AttributeKind::Custom(attr);
|
||||
let inner = kind.to_inner();
|
||||
Self { kind, inner }
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &Pkcs12AttributeKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &Pkcs12AttributeAsn1 {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Pkcs12AttributeKind {
|
||||
FriendlyName(BmpString),
|
||||
LocalKeyId(Vec<u8>),
|
||||
Custom(CustomPkcs12Attribute),
|
||||
}
|
||||
|
||||
impl Pkcs12AttributeKind {
|
||||
pub(crate) fn to_inner(&self) -> Pkcs12AttributeAsn1 {
|
||||
match self {
|
||||
Self::FriendlyName(value) => Pkcs12AttributeAsn1::FriendlyName(value.clone()),
|
||||
Self::LocalKeyId(value) => Pkcs12AttributeAsn1::LocalKeyId(OctetStringAsn1::from(value.clone())),
|
||||
Self::Custom(value) => Pkcs12AttributeAsn1::Unknown {
|
||||
oid: value.oid.clone(),
|
||||
value: value.value.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pkcs12AttributeAsn1> for Pkcs12AttributeKind {
|
||||
fn from(value: Pkcs12AttributeAsn1) -> Self {
|
||||
match value {
|
||||
Pkcs12AttributeAsn1::FriendlyName(value) => Self::FriendlyName(value),
|
||||
Pkcs12AttributeAsn1::LocalKeyId(value) => Self::LocalKeyId(value.0),
|
||||
Pkcs12AttributeAsn1::Unknown { oid, value } => CustomPkcs12Attribute { oid, value }.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CustomPkcs12Attribute> for Pkcs12AttributeKind {
|
||||
fn from(value: CustomPkcs12Attribute) -> Self {
|
||||
Self::Custom(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CustomPkcs12Attribute {
|
||||
oid: ObjectIdentifier,
|
||||
value: Vec<Asn1RawDer>,
|
||||
}
|
||||
|
||||
impl CustomPkcs12Attribute {
|
||||
/// Get attribute oid.
|
||||
pub fn oid(&self) -> &ObjectIdentifier {
|
||||
&self.oid
|
||||
}
|
||||
|
||||
pub fn new_empty(oid: ObjectIdentifier) -> Self {
|
||||
Self { oid, value: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn has_value(&self) -> bool {
|
||||
!self.value.is_empty()
|
||||
}
|
||||
|
||||
pub fn new_raw(oid: ObjectIdentifier, value: Vec<Asn1RawDer>) -> Self {
|
||||
Self { oid, value }
|
||||
}
|
||||
|
||||
/// Creates a new custom attribute from any DER-serializable value. Ut is advised to use types
|
||||
/// from `picky-asn1-der` crate.
|
||||
pub fn new_single_value<T: Serialize>(oid: ObjectIdentifier, value: &T) -> Result<Self, Pkcs12Error> {
|
||||
let encoded = picky_asn1_der::to_vec(value)?;
|
||||
Ok(Self {
|
||||
oid,
|
||||
value: vec![Asn1RawDer(encoded)],
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new custom attribute from multiple any DER-serializable value (PKCS#12 allows
|
||||
/// attribute to have list of values). It is advised to use types from `picky-asn1-der` crate.
|
||||
pub fn new_multiple_values<'a, T: Serialize + 'a>(
|
||||
oid: ObjectIdentifier,
|
||||
values: impl IntoIterator<Item = &'a T>,
|
||||
) -> Result<Self, Pkcs12Error> {
|
||||
let mut encoded_values = Vec::new();
|
||||
for value in values {
|
||||
let encoded = picky_asn1_der::to_vec(value)?;
|
||||
encoded_values.push(Asn1RawDer(encoded));
|
||||
}
|
||||
Ok(Self {
|
||||
oid,
|
||||
value: encoded_values,
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert inner value to any DER-deserializable value. It is advised to use types from
|
||||
/// `picky-asn1-der` crate.
|
||||
pub fn to_single_value<'a, T: Deserialize<'a>>(&'a self) -> Result<T, Pkcs12Error> {
|
||||
if self.value.len() != 1 {
|
||||
return Err(Pkcs12Error::UnexpectedAttributeValuesCount {
|
||||
expected: 1,
|
||||
actual: self.value.len(),
|
||||
});
|
||||
}
|
||||
let deserialized = picky_asn1_der::from_bytes(&self.value[0].0)?;
|
||||
Ok(deserialized)
|
||||
}
|
||||
|
||||
/// Convert inner value to multiple DER-deserializable values. It is advised to use types from
|
||||
/// `picky-asn1-der` crate.
|
||||
pub fn to_multiple_values<'a, T: Deserialize<'a>>(&'a self) -> Result<Vec<T>, Pkcs12Error> {
|
||||
let values_result: Result<Vec<_>, _> = self
|
||||
.value
|
||||
.iter()
|
||||
.map(|value| picky_asn1_der::from_bytes(&value.0))
|
||||
.collect();
|
||||
values_result.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn raw_data(&self) -> &[Asn1RawDer] {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
use expect_test::expect;
|
||||
|
||||
fn fake_oid() -> ObjectIdentifier {
|
||||
"1.3.6.1.4.1.311.17.1".to_string().try_into().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_custom_attribute_roundtrip() {
|
||||
let value = BmpString::from_str("Microsoft Software Key Storage Provider").unwrap();
|
||||
let attr = CustomPkcs12Attribute::new_single_value(fake_oid(), &value).unwrap();
|
||||
let decoded = attr.to_single_value::<BmpString>().unwrap();
|
||||
assert_eq!(decoded, value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_custom_attribute_roundtrip_no_value() {
|
||||
let attr = CustomPkcs12Attribute::new_empty(fake_oid());
|
||||
assert!(!attr.has_value());
|
||||
let decoded = attr.to_single_value::<BmpString>();
|
||||
expect![[r#"
|
||||
Err(
|
||||
UnexpectedAttributeValuesCount {
|
||||
expected: 1,
|
||||
actual: 0,
|
||||
},
|
||||
)
|
||||
"#]]
|
||||
.assert_debug_eq(&decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_custom_attribute_roundtrip_too_many_values() {
|
||||
let value = vec![OctetStringAsn1(vec![0x01]), OctetStringAsn1(vec![0x02])];
|
||||
let attr = CustomPkcs12Attribute::new_multiple_values(fake_oid(), &value).unwrap();
|
||||
let decoded = attr.to_single_value::<BmpString>();
|
||||
expect![[r#"
|
||||
Err(
|
||||
UnexpectedAttributeValuesCount {
|
||||
expected: 1,
|
||||
actual: 2,
|
||||
},
|
||||
)
|
||||
"#]]
|
||||
.assert_debug_eq(&decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_custom_attributes_roundtrip() {
|
||||
let value = vec![
|
||||
OctetStringAsn1(vec![0x01, 0x02, 0x03]),
|
||||
OctetStringAsn1(vec![0x04, 0x05, 0x06]),
|
||||
];
|
||||
|
||||
let attr = CustomPkcs12Attribute::new_multiple_values(fake_oid(), &value).unwrap();
|
||||
let decoded = attr.to_multiple_values::<OctetStringAsn1>().unwrap();
|
||||
assert_eq!(decoded, value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_custom_attributes_empty_list_allowed() {
|
||||
let value: Vec<OctetStringAsn1> = vec![];
|
||||
let attr = CustomPkcs12Attribute::new_multiple_values(fake_oid(), &value).unwrap();
|
||||
let decoded = attr.to_multiple_values::<OctetStringAsn1>().unwrap();
|
||||
assert_eq!(decoded, value);
|
||||
}
|
||||
}
|
||||
+674
@@ -0,0 +1,674 @@
|
||||
use crate::pkcs12::{Pbkdf1Usage, Pkcs12Error, Pkcs12HashAlgorithm, pbkdf1};
|
||||
use picky_asn1::restricted_string::BmpString;
|
||||
use picky_asn1::wrapper::OctetStringAsn1;
|
||||
pub use picky_asn1_x509::pkcs12::Pbes1AlgorithmKind as Pbes1Cipher;
|
||||
use picky_asn1_x509::pkcs12::{
|
||||
Pbes1Params as Pbes1ParamsAsn1, Pbes2AesCbcEncryption as Pbes2AesCbcEncryptionAsn1,
|
||||
Pbes2EncryptionScheme as Pbes2EncryptionSchemeAsn1, Pbes2KeyDerivationFunc as Pbes2KeyDerivationFuncAsn1,
|
||||
Pbes2Params as Pbes2ParamsAsn1, Pbkdf2Params as Pbkdf2ParamsAsn1, Pbkdf2Prf as Pbkdf2PrfAsn1,
|
||||
Pbkdf2SaltSource as Pbkdf2SaltSourceAsn1, Pkcs12EncryptionAlgorithm as Pkcs12EncryptionAsn1,
|
||||
};
|
||||
use rand::rngs::{StdRng, SysRng};
|
||||
use rand_core::SeedableRng as _;
|
||||
use std::str::FromStr as _;
|
||||
|
||||
/// Same default KDF iterations as in OpenSSL
|
||||
const DEFAULT_KDF_ITERATIONS: usize = 2048;
|
||||
const DEFAULT_SALT_SIZE: usize = 8;
|
||||
const AES_BLOCK_SIZE: usize = 16;
|
||||
|
||||
/// Crypto operations context for PFX file parsing/building. Contains password inside as a secure
|
||||
/// string and RNG.
|
||||
pub struct Pkcs12CryptoContext {
|
||||
password: zeroize::Zeroizing<String>,
|
||||
rng: Box<dyn rand_core::Rng>,
|
||||
}
|
||||
|
||||
impl Pkcs12CryptoContext {
|
||||
/// Creates new context with given password and default
|
||||
pub fn new_with_password(password: &str) -> Result<Self, Pkcs12Error> {
|
||||
Ok(Self {
|
||||
password: password.to_string().into(),
|
||||
rng: Box::new(StdRng::try_from_rng(&mut SysRng)?),
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets RNG for this context
|
||||
pub fn with_rng(mut self, rng: impl rand::CryptoRng + 'static) -> Self {
|
||||
self.rng = Box::new(rng);
|
||||
self
|
||||
}
|
||||
|
||||
/// Creates new context with empty password and default RNG
|
||||
pub fn new_without_password() -> Result<Self, Pkcs12Error> {
|
||||
Ok(Self {
|
||||
password: String::new().into(),
|
||||
rng: Box::new(StdRng::try_from_rng(&mut SysRng)?),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns password in PBES1 password representation - UCS2 encoded string with null terminator
|
||||
pub(crate) fn password_bytes_pbes1(&self) -> Result<zeroize::Zeroizing<Vec<u8>>, Pkcs12Error> {
|
||||
let mut bmp = zeroize::Zeroizing::new(BmpString::from_str(&self.password)?.into_bytes());
|
||||
bmp.extend_from_slice(&[0, 0]);
|
||||
|
||||
Ok(bmp)
|
||||
}
|
||||
|
||||
/// Returns password in PBES2 password representation - UTF8 encoded string
|
||||
pub(crate) fn password_bytes_pbes2(&self) -> &[u8] {
|
||||
self.password.as_bytes()
|
||||
}
|
||||
|
||||
pub(crate) fn generate_bytes(&mut self, len: usize) -> Vec<u8> {
|
||||
let mut data = vec![0u8; len];
|
||||
self.rng.fill_bytes(&mut data);
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
/// This type holds all information required to encrypt/decrypt data in PKCS#12 file, including
|
||||
/// encryption algorithm, salt, IV and KDF iterations.
|
||||
///
|
||||
/// This type is not cloneable, because it is intended to be used only once for each encryptable
|
||||
/// object. If for some reason you need to have exactly the same salt/IV values for multiple
|
||||
/// encryptable objects, you should create crypto context with custom RNG set to same seed and
|
||||
/// create multiple Pkcs12Encryption objects from it.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Pkcs12Encryption {
|
||||
kind: Pkcs12EncryptionKind,
|
||||
inner: Pkcs12EncryptionAsn1,
|
||||
}
|
||||
|
||||
impl Pkcs12Encryption {
|
||||
/// Clone-like operation only could be performed internally when cloning higher level structures.
|
||||
/// Pkcs12Encryption non-cloneable nature is intentional to provide hint to the user that this
|
||||
/// structure should be used only once for each encryptable object.
|
||||
pub(crate) fn duplicate(&self) -> Self {
|
||||
Self {
|
||||
kind: self.kind.clone(),
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_asn1(inner: Pkcs12EncryptionAsn1) -> Result<Self, Pkcs12Error> {
|
||||
let kind = match &inner {
|
||||
Pkcs12EncryptionAsn1::Pbes1 { kind, params } => Pkcs12EncryptionKind::Pbes1(Pbes1Encryption {
|
||||
cipher: *kind,
|
||||
kdf_iterations: Some(params.iterations),
|
||||
}),
|
||||
Pkcs12EncryptionAsn1::Pbes2(Pbes2ParamsAsn1 {
|
||||
key_derivation_func:
|
||||
Pbes2KeyDerivationFuncAsn1::Pbkdf2(Pbkdf2ParamsAsn1 {
|
||||
iteration_count, prf, ..
|
||||
}),
|
||||
encryption_scheme: Pbes2EncryptionSchemeAsn1::AesCbc { kind, .. },
|
||||
}) => {
|
||||
let hmac_kdf = match &prf {
|
||||
Some(algorithm) => Pkcs12HashAlgorithm::from_asn1_pbkdf2_prf(algorithm)?,
|
||||
None => Pkcs12HashAlgorithm::Sha1,
|
||||
};
|
||||
Pkcs12EncryptionKind::Pbes2(Pbes2Encryption {
|
||||
cipher: (*kind).into(),
|
||||
hmac_kdf,
|
||||
kdf_iterations: Some(*iteration_count),
|
||||
})
|
||||
}
|
||||
_ => Pkcs12EncryptionKind::Unknown,
|
||||
};
|
||||
|
||||
Ok(Self { kind, inner })
|
||||
}
|
||||
|
||||
/// Create new legacy PBES1 encryption (Not recommended for new files)
|
||||
pub fn new_pbes1(encryption: Pbes1Encryption, context: &mut Pkcs12CryptoContext) -> Self {
|
||||
let salt = context.generate_bytes(DEFAULT_SALT_SIZE);
|
||||
let inner = Pkcs12EncryptionAsn1::Pbes1 {
|
||||
kind: encryption.cipher,
|
||||
params: Pbes1ParamsAsn1 {
|
||||
salt: OctetStringAsn1(salt),
|
||||
iterations: encryption.kdf_iterations.unwrap_or(DEFAULT_KDF_ITERATIONS as u32),
|
||||
},
|
||||
};
|
||||
|
||||
Self {
|
||||
kind: Pkcs12EncryptionKind::Pbes1(encryption),
|
||||
inner,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new PBES2 encryption (It is advised to use PBES2 for new files)
|
||||
pub fn new_pbes2(encryption: Pbes2Encryption, context: &mut Pkcs12CryptoContext) -> Self {
|
||||
let iv = context.generate_bytes(AES_BLOCK_SIZE);
|
||||
let encryption_scheme = Pbes2EncryptionSchemeAsn1::AesCbc {
|
||||
kind: encryption.cipher.into(),
|
||||
iv: OctetStringAsn1(iv),
|
||||
};
|
||||
|
||||
// Skip serialization if set to SHA1 as specified in RFC
|
||||
let prf = match Pbkdf2PrfAsn1::from(encryption.hmac_kdf) {
|
||||
Pbkdf2PrfAsn1::HmacWithSha1 => None,
|
||||
value => Some(value),
|
||||
};
|
||||
|
||||
let kdf_params = Pbkdf2ParamsAsn1 {
|
||||
salt: Pbkdf2SaltSourceAsn1::Specified(OctetStringAsn1(context.generate_bytes(DEFAULT_SALT_SIZE))),
|
||||
iteration_count: encryption.kdf_iterations.unwrap_or(DEFAULT_KDF_ITERATIONS as u32),
|
||||
// key length is not set by most implementations
|
||||
key_length: None,
|
||||
prf,
|
||||
};
|
||||
|
||||
let pbes2_params = Pbes2ParamsAsn1 {
|
||||
key_derivation_func: Pbes2KeyDerivationFuncAsn1::Pbkdf2(kdf_params),
|
||||
encryption_scheme,
|
||||
};
|
||||
|
||||
let inner = Pkcs12EncryptionAsn1::Pbes2(pbes2_params);
|
||||
|
||||
Self {
|
||||
kind: Pkcs12EncryptionKind::Pbes2(encryption),
|
||||
inner,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed encryption representation
|
||||
pub fn kind(&self) -> &Pkcs12EncryptionKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &Pkcs12EncryptionAsn1 {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub(crate) fn decrypt(&self, data: &[u8], context: &Pkcs12CryptoContext) -> Result<Vec<u8>, Pkcs12Error> {
|
||||
match self.inner() {
|
||||
Pkcs12EncryptionAsn1::Pbes1 { kind, params } => {
|
||||
let password = context.password_bytes_pbes1()?;
|
||||
decrypt_pbes1(
|
||||
*kind,
|
||||
password.as_slice(),
|
||||
params.salt.as_slice(),
|
||||
params.iterations as usize,
|
||||
data,
|
||||
)
|
||||
}
|
||||
Pkcs12EncryptionAsn1::Pbes2(params) => {
|
||||
let password = context.password_bytes_pbes2();
|
||||
decrypt_pbes2(params, password, data)
|
||||
}
|
||||
Pkcs12EncryptionAsn1::Unknown(raw) => {
|
||||
let oid = raw.algorithm().clone();
|
||||
Err(Pkcs12Error::NotSupportedAlgorithm {
|
||||
algorithm: super::UnsupportedPkcs12Algorithm::Oid(oid),
|
||||
context: "decryption".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn encrypt(&self, data: &[u8], context: &Pkcs12CryptoContext) -> Result<Vec<u8>, Pkcs12Error> {
|
||||
match self.inner() {
|
||||
Pkcs12EncryptionAsn1::Pbes1 { kind, params } => {
|
||||
let password = context.password_bytes_pbes1()?;
|
||||
encrypt_pbes1(
|
||||
*kind,
|
||||
password.as_slice(),
|
||||
params.salt.as_slice(),
|
||||
params.iterations as usize,
|
||||
data,
|
||||
)
|
||||
}
|
||||
Pkcs12EncryptionAsn1::Pbes2(params) => {
|
||||
let password = context.password_bytes_pbes2();
|
||||
encrypt_pbes2(params, password, data)
|
||||
}
|
||||
Pkcs12EncryptionAsn1::Unknown(raw) => {
|
||||
let oid = raw.algorithm().clone();
|
||||
Err(Pkcs12Error::NotSupportedAlgorithm {
|
||||
algorithm: super::UnsupportedPkcs12Algorithm::Oid(oid),
|
||||
context: "encryption".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Supported PKCS12 encryption algorithm descriptor.
|
||||
/// If parsed PFX file contains unknown encryption algorithm, [`Pkcs12EncryptionKind::Unknown`]
|
||||
/// variant is returned instead. If such encryption is encountered, then we can't use encrypted PFX
|
||||
/// nodes, but unencrypted data still could be extracted.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Pkcs12EncryptionKind {
|
||||
Pbes1(Pbes1Encryption),
|
||||
Pbes2(Pbes2Encryption),
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// PBES1 encryption descriptor.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Pbes1Encryption {
|
||||
cipher: Pbes1Cipher,
|
||||
kdf_iterations: Option<u32>,
|
||||
}
|
||||
|
||||
impl Pbes1Encryption {
|
||||
/// Creates new PBES1 encryption descriptor with given cipher algorithm.
|
||||
pub fn new(cipher: Pbes1Cipher) -> Self {
|
||||
Self {
|
||||
cipher,
|
||||
kdf_iterations: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets KDF iteraions count to `iterations`. If not set, [`DEFAULT_KDF_ITERATIONS`] is used
|
||||
/// instead.
|
||||
pub fn with_kdf_iterations(mut self, iterations: u32) -> Self {
|
||||
self.kdf_iterations = Some(iterations);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// PBES2 encryption descriptor.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Pbes2Encryption {
|
||||
cipher: Pbes2Cipher,
|
||||
hmac_kdf: Pkcs12HashAlgorithm,
|
||||
kdf_iterations: Option<u32>,
|
||||
}
|
||||
|
||||
impl Pbes2Encryption {
|
||||
/// Creates new PBES2 encryption descriptor with given kdf and cipher algorithms.
|
||||
pub fn new(cipher: Pbes2Cipher, hmac_kdf: Pkcs12HashAlgorithm) -> Self {
|
||||
Self {
|
||||
cipher,
|
||||
hmac_kdf,
|
||||
kdf_iterations: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets KDF iteraions count to `iterations`. If not set, [`DEFAULT_KDF_ITERATIONS`] is used
|
||||
/// instead.
|
||||
pub fn with_kdf_iterations(mut self, iterations: u32) -> Self {
|
||||
self.kdf_iterations = Some(iterations);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// PBES2 cipher algorithm
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Pbes2Cipher {
|
||||
Aes128Cbc,
|
||||
Aes192Cbc,
|
||||
Aes256Cbc,
|
||||
}
|
||||
|
||||
impl Pbes2Cipher {
|
||||
/// Returns cipher key size in bytes.
|
||||
pub fn key_size(self) -> usize {
|
||||
match self {
|
||||
Self::Aes128Cbc => 16,
|
||||
Self::Aes192Cbc => 24,
|
||||
Self::Aes256Cbc => 32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pbes2Cipher> for Pbes2AesCbcEncryptionAsn1 {
|
||||
fn from(value: Pbes2Cipher) -> Self {
|
||||
match value {
|
||||
Pbes2Cipher::Aes128Cbc => Self::Aes128,
|
||||
Pbes2Cipher::Aes192Cbc => Self::Aes192,
|
||||
Pbes2Cipher::Aes256Cbc => Self::Aes256,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pbes2AesCbcEncryptionAsn1> for Pbes2Cipher {
|
||||
fn from(value: Pbes2AesCbcEncryptionAsn1) -> Self {
|
||||
match value {
|
||||
Pbes2AesCbcEncryptionAsn1::Aes128 => Self::Aes128Cbc,
|
||||
Pbes2AesCbcEncryptionAsn1::Aes192 => Self::Aes192Cbc,
|
||||
Pbes2AesCbcEncryptionAsn1::Aes256 => Self::Aes256Cbc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pkcs12HashAlgorithm> for Pbkdf2PrfAsn1 {
|
||||
fn from(value: Pkcs12HashAlgorithm) -> Self {
|
||||
match value {
|
||||
Pkcs12HashAlgorithm::Sha1 => Self::HmacWithSha1,
|
||||
Pkcs12HashAlgorithm::Sha224 => Self::HmacWithSha224,
|
||||
Pkcs12HashAlgorithm::Sha256 => Self::HmacWithSha256,
|
||||
Pkcs12HashAlgorithm::Sha384 => Self::HmacWithSha384,
|
||||
Pkcs12HashAlgorithm::Sha512 => Self::HmacWithSha512,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Pbes2CipherInputs {
|
||||
key: Vec<u8>,
|
||||
iv: Vec<u8>,
|
||||
cipher: Pbes2Cipher,
|
||||
}
|
||||
|
||||
fn prepare_pbes2_cipher_inputs(
|
||||
params: &Pbes2ParamsAsn1,
|
||||
password: &[u8],
|
||||
cipher_context: &str,
|
||||
) -> Result<Pbes2CipherInputs, Pkcs12Error> {
|
||||
let (salt, kdf_iterations, prf) = match ¶ms.key_derivation_func {
|
||||
Pbes2KeyDerivationFuncAsn1::Pbkdf2(kdf) => {
|
||||
let salt = match &kdf.salt {
|
||||
Pbkdf2SaltSourceAsn1::Specified(salt) => salt.0.clone(),
|
||||
Pbkdf2SaltSourceAsn1::OtherSource(raw) => {
|
||||
let oid = raw.algorithm().clone();
|
||||
return Err(Pkcs12Error::NotSupportedAlgorithm {
|
||||
algorithm: super::UnsupportedPkcs12Algorithm::Oid(oid),
|
||||
context: format!("pbes2 {cipher_context} (kdf salt source)"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let prf = match &kdf.prf {
|
||||
None => Pkcs12HashAlgorithm::Sha1,
|
||||
Some(Pbkdf2PrfAsn1::HmacWithSha1) => Pkcs12HashAlgorithm::Sha1,
|
||||
Some(Pbkdf2PrfAsn1::HmacWithSha224) => Pkcs12HashAlgorithm::Sha224,
|
||||
Some(Pbkdf2PrfAsn1::HmacWithSha256) => Pkcs12HashAlgorithm::Sha256,
|
||||
Some(Pbkdf2PrfAsn1::HmacWithSha384) => Pkcs12HashAlgorithm::Sha384,
|
||||
Some(Pbkdf2PrfAsn1::HmacWithSha512) => Pkcs12HashAlgorithm::Sha512,
|
||||
Some(Pbkdf2PrfAsn1::Unknown(raw)) => {
|
||||
let oid = raw.algorithm().clone();
|
||||
return Err(Pkcs12Error::NotSupportedAlgorithm {
|
||||
algorithm: super::UnsupportedPkcs12Algorithm::Oid(oid),
|
||||
context: format!("pbes2 {cipher_context} (kdf prf)"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
(salt, kdf.iteration_count, prf)
|
||||
}
|
||||
Pbes2KeyDerivationFuncAsn1::Unknown(raw) => {
|
||||
let oid = raw.algorithm().clone();
|
||||
return Err(Pkcs12Error::NotSupportedAlgorithm {
|
||||
algorithm: super::UnsupportedPkcs12Algorithm::Oid(oid),
|
||||
context: format!("pbes2 {cipher_context} (kdf)"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let (cipher, iv) = match ¶ms.encryption_scheme {
|
||||
Pbes2EncryptionSchemeAsn1::AesCbc { kind, iv } => (Pbes2Cipher::from(*kind), iv.0.clone()),
|
||||
Pbes2EncryptionSchemeAsn1::Unknown(raw) => {
|
||||
let oid = raw.algorithm().clone();
|
||||
return Err(Pkcs12Error::NotSupportedAlgorithm {
|
||||
algorithm: super::UnsupportedPkcs12Algorithm::Oid(oid),
|
||||
context: format!("pbes2 {cipher_context} (cipher)"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let calculate_kdf = match prf {
|
||||
Pkcs12HashAlgorithm::Sha1 => pbkdf2::pbkdf2_hmac::<sha1::Sha1>,
|
||||
Pkcs12HashAlgorithm::Sha224 => pbkdf2::pbkdf2_hmac::<sha2::Sha224>,
|
||||
Pkcs12HashAlgorithm::Sha256 => pbkdf2::pbkdf2_hmac::<sha2::Sha256>,
|
||||
Pkcs12HashAlgorithm::Sha384 => pbkdf2::pbkdf2_hmac::<sha2::Sha384>,
|
||||
Pkcs12HashAlgorithm::Sha512 => pbkdf2::pbkdf2_hmac::<sha2::Sha512>,
|
||||
};
|
||||
|
||||
let mut key = vec![0u8; cipher.key_size()];
|
||||
calculate_kdf(password, salt.as_slice(), kdf_iterations, key.as_mut_slice());
|
||||
|
||||
Ok(Pbes2CipherInputs { key, iv, cipher })
|
||||
}
|
||||
|
||||
fn decrypt_pbes2(params: &Pbes2ParamsAsn1, password: &[u8], data: &[u8]) -> Result<Vec<u8>, Pkcs12Error> {
|
||||
let Pbes2CipherInputs { key, iv, cipher } = prepare_pbes2_cipher_inputs(params, password, "decryption")?;
|
||||
|
||||
use aes::cipher::BlockModeDecrypt;
|
||||
use cbc::Decryptor;
|
||||
use cbc::cipher::KeyIvInit;
|
||||
use cbc::cipher::block_padding::Pkcs7;
|
||||
|
||||
let decrypted = match cipher {
|
||||
Pbes2Cipher::Aes128Cbc => {
|
||||
use aes::Aes128;
|
||||
type Aes128Cbc = Decryptor<Aes128>;
|
||||
|
||||
let aes = Aes128Cbc::new_from_slices(key.as_slice(), iv.as_slice()).map_err(|_| Pkcs12Error::Pbes2 {
|
||||
context: "AES128 decryptor initialization failed".to_string(),
|
||||
})?;
|
||||
|
||||
aes.decrypt_padded_vec::<Pkcs7>(data).map_err(|_| Pkcs12Error::Pbes2 {
|
||||
context: "AES128 decryption with padding failed".to_string(),
|
||||
})?
|
||||
}
|
||||
Pbes2Cipher::Aes192Cbc => {
|
||||
use aes::Aes192;
|
||||
type Aes192Cbc = Decryptor<Aes192>;
|
||||
|
||||
let aes = Aes192Cbc::new_from_slices(key.as_slice(), iv.as_slice()).map_err(|_| Pkcs12Error::Pbes2 {
|
||||
context: "AES192 decryptor initialization failed".to_string(),
|
||||
})?;
|
||||
|
||||
aes.decrypt_padded_vec::<Pkcs7>(data).map_err(|_| Pkcs12Error::Pbes2 {
|
||||
context: "AES192 decryption with padding failed".to_string(),
|
||||
})?
|
||||
}
|
||||
Pbes2Cipher::Aes256Cbc => {
|
||||
use aes::Aes256;
|
||||
type Aes256Cbc = Decryptor<Aes256>;
|
||||
|
||||
let aes = Aes256Cbc::new_from_slices(key.as_slice(), iv.as_slice()).map_err(|_| Pkcs12Error::Pbes2 {
|
||||
context: "AES256 decryptor initialization failed".to_string(),
|
||||
})?;
|
||||
|
||||
aes.decrypt_padded_vec::<Pkcs7>(data).map_err(|_| Pkcs12Error::Pbes2 {
|
||||
context: "AES256 decryption with padding failed".to_string(),
|
||||
})?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(decrypted)
|
||||
}
|
||||
|
||||
fn encrypt_pbes2(params: &Pbes2ParamsAsn1, password: &[u8], data: &[u8]) -> Result<Vec<u8>, Pkcs12Error> {
|
||||
let Pbes2CipherInputs { key, iv, cipher } = prepare_pbes2_cipher_inputs(params, password, "encryption")?;
|
||||
|
||||
use aes::cipher::BlockModeEncrypt;
|
||||
use cbc::Encryptor;
|
||||
use cbc::cipher::KeyIvInit;
|
||||
use cbc::cipher::block_padding::Pkcs7;
|
||||
|
||||
let encrypted = match cipher {
|
||||
Pbes2Cipher::Aes128Cbc => {
|
||||
use aes::Aes128;
|
||||
type Aes128Cbc = Encryptor<Aes128>;
|
||||
|
||||
let aes = Aes128Cbc::new_from_slices(key.as_slice(), iv.as_slice()).map_err(|_| Pkcs12Error::Pbes2 {
|
||||
context: "AES128 encryptor initialization failed".to_string(),
|
||||
})?;
|
||||
|
||||
aes.encrypt_padded_vec::<Pkcs7>(data)
|
||||
}
|
||||
Pbes2Cipher::Aes192Cbc => {
|
||||
use aes::Aes192;
|
||||
type Aes192Cbc = Encryptor<Aes192>;
|
||||
|
||||
let aes = Aes192Cbc::new_from_slices(key.as_slice(), iv.as_slice()).map_err(|_| Pkcs12Error::Pbes2 {
|
||||
context: "AES192 encryptor initialization failed".to_string(),
|
||||
})?;
|
||||
|
||||
aes.encrypt_padded_vec::<Pkcs7>(data)
|
||||
}
|
||||
Pbes2Cipher::Aes256Cbc => {
|
||||
use aes::Aes256;
|
||||
type Aes256Cbc = Encryptor<Aes256>;
|
||||
|
||||
let aes = Aes256Cbc::new_from_slices(key.as_slice(), iv.as_slice()).map_err(|_| Pkcs12Error::Pbes2 {
|
||||
context: "AES256 encryptor initialization failed".to_string(),
|
||||
})?;
|
||||
|
||||
aes.encrypt_padded_vec::<Pkcs7>(data)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(encrypted)
|
||||
}
|
||||
|
||||
fn generate_pbes1_key_and_iv(
|
||||
cipher: Pbes1Cipher,
|
||||
password: &[u8],
|
||||
salt: &[u8],
|
||||
kdf_iterations: usize,
|
||||
) -> (Vec<u8>, Vec<u8>) {
|
||||
let (key_size, iv_size) = match cipher {
|
||||
Pbes1Cipher::ShaAnd40BitRc2Cbc => (5, 8),
|
||||
Pbes1Cipher::ShaAnd3Key3DesCbc => (24, 8),
|
||||
};
|
||||
|
||||
let key = pbkdf1(
|
||||
Pkcs12HashAlgorithm::Sha1,
|
||||
password,
|
||||
salt,
|
||||
kdf_iterations,
|
||||
Pbkdf1Usage::Key,
|
||||
key_size,
|
||||
);
|
||||
let iv = pbkdf1(
|
||||
Pkcs12HashAlgorithm::Sha1,
|
||||
password,
|
||||
salt,
|
||||
kdf_iterations,
|
||||
Pbkdf1Usage::Iv,
|
||||
iv_size,
|
||||
);
|
||||
|
||||
(key, iv)
|
||||
}
|
||||
|
||||
fn encrypt_pbes1(
|
||||
scheme: Pbes1Cipher,
|
||||
password: &[u8],
|
||||
salt: &[u8],
|
||||
kdf_iterations: usize,
|
||||
data: &[u8],
|
||||
) -> Result<Vec<u8>, Pkcs12Error> {
|
||||
use cbc::Encryptor;
|
||||
use cbc::cipher::block_padding::Pkcs7;
|
||||
use cbc::cipher::{BlockModeEncrypt, KeyIvInit};
|
||||
|
||||
let (dk, iv) = generate_pbes1_key_and_iv(scheme, password, salt, kdf_iterations);
|
||||
|
||||
match scheme {
|
||||
Pbes1Cipher::ShaAnd40BitRc2Cbc => {
|
||||
use rc2::Rc2;
|
||||
type Rc2Cbc = Encryptor<Rc2>;
|
||||
|
||||
let rc2 = Rc2Cbc::new_from_slices(&dk, &iv).map_err(|_| Pkcs12Error::Pbes1 {
|
||||
context: "RC2 encryption initialization failed".to_string(),
|
||||
})?;
|
||||
Ok(rc2.encrypt_padded_vec::<Pkcs7>(data))
|
||||
}
|
||||
Pbes1Cipher::ShaAnd3Key3DesCbc => {
|
||||
use des::TdesEde3;
|
||||
type TDesCbc = Encryptor<TdesEde3>;
|
||||
|
||||
let tdes = TDesCbc::new_from_slices(&dk, &iv).map_err(|_| Pkcs12Error::Pbes1 {
|
||||
context: "3DES encryptor initialization failed".to_string(),
|
||||
})?;
|
||||
Ok(tdes.encrypt_padded_vec::<Pkcs7>(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypt_pbes1(
|
||||
scheme: Pbes1Cipher,
|
||||
password: &[u8],
|
||||
salt: &[u8],
|
||||
kdf_iterations: usize,
|
||||
data: &[u8],
|
||||
) -> Result<Vec<u8>, Pkcs12Error> {
|
||||
use cbc::Decryptor;
|
||||
use cbc::cipher::block_padding::Pkcs7;
|
||||
use cbc::cipher::{BlockModeDecrypt, KeyIvInit};
|
||||
|
||||
let (dk, iv) = generate_pbes1_key_and_iv(scheme, password, salt, kdf_iterations);
|
||||
|
||||
match scheme {
|
||||
Pbes1Cipher::ShaAnd40BitRc2Cbc => {
|
||||
use rc2::Rc2;
|
||||
type Rc2Cbc = Decryptor<Rc2>;
|
||||
|
||||
let rc2 = Rc2Cbc::new_from_slices(&dk, &iv).map_err(|_| Pkcs12Error::Pbes1 {
|
||||
context: "RC2 decryptor initialization failed".to_string(),
|
||||
})?;
|
||||
rc2.decrypt_padded_vec::<Pkcs7>(data).map_err(|_| Pkcs12Error::Pbes1 {
|
||||
context: "RC2 decryption with padding failed".to_string(),
|
||||
})
|
||||
}
|
||||
Pbes1Cipher::ShaAnd3Key3DesCbc => {
|
||||
use des::TdesEde3;
|
||||
type TDesCbc = Decryptor<TdesEde3>;
|
||||
|
||||
let tdes = TDesCbc::new_from_slices(&dk, &iv).map_err(|_| Pkcs12Error::Pbes1 {
|
||||
context: "3DES decryptor initialization failed".to_string(),
|
||||
})?;
|
||||
tdes.decrypt_padded_vec::<Pkcs7>(data).map_err(|_| Pkcs12Error::Pbes1 {
|
||||
context: "3DES decryption with padding failed".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn pbes1_3des_roundtrip() {
|
||||
let password = b"\0a\0b\0c\0\0";
|
||||
let salt = (0..8).collect::<Vec<u8>>();
|
||||
let iterations = 2000;
|
||||
let data = (0..123).collect::<Vec<u8>>();
|
||||
let encrypted = encrypt_pbes1(Pbes1Cipher::ShaAnd3Key3DesCbc, password, &salt, iterations, &data).unwrap();
|
||||
let decrypted = decrypt_pbes1(Pbes1Cipher::ShaAnd3Key3DesCbc, password, &salt, iterations, &encrypted).unwrap();
|
||||
assert_eq!(decrypted, data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pbes1_rc2_roundtrip() {
|
||||
let password = b"\0b\0c\0a\0\0";
|
||||
let salt = (0..8).collect::<Vec<u8>>();
|
||||
let iterations = 2048;
|
||||
let data = (0..124).collect::<Vec<u8>>();
|
||||
let encrypted = encrypt_pbes1(Pbes1Cipher::ShaAnd40BitRc2Cbc, password, &salt, iterations, &data).unwrap();
|
||||
let decrypted = decrypt_pbes1(Pbes1Cipher::ShaAnd40BitRc2Cbc, password, &salt, iterations, &encrypted).unwrap();
|
||||
assert_eq!(decrypted, data);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Pbes2AesCbcEncryptionAsn1::Aes128)]
|
||||
#[case(Pbes2AesCbcEncryptionAsn1::Aes192)]
|
||||
#[case(Pbes2AesCbcEncryptionAsn1::Aes256)]
|
||||
fn pbes2_aes256_roundtrip(#[case] aes: Pbes2AesCbcEncryptionAsn1) {
|
||||
let password = b"test";
|
||||
let data = (0..123).collect::<Vec<u8>>();
|
||||
let params = Pbes2ParamsAsn1 {
|
||||
key_derivation_func: Pbes2KeyDerivationFuncAsn1::Pbkdf2(Pbkdf2ParamsAsn1 {
|
||||
salt: Pbkdf2SaltSourceAsn1::Specified(OctetStringAsn1((0..8).collect())),
|
||||
iteration_count: 2000,
|
||||
key_length: None,
|
||||
prf: Some(Pbkdf2PrfAsn1::HmacWithSha256),
|
||||
}),
|
||||
encryption_scheme: Pbes2EncryptionSchemeAsn1::AesCbc {
|
||||
kind: aes,
|
||||
iv: OctetStringAsn1((0..16).collect()),
|
||||
},
|
||||
};
|
||||
let encrypted = encrypt_pbes2(¶ms, password, &data).unwrap();
|
||||
let decrypted = decrypt_pbes2(¶ms, password, &encrypted).unwrap();
|
||||
assert_eq!(decrypted, data);
|
||||
}
|
||||
}
|
||||
Vendored
+196
@@ -0,0 +1,196 @@
|
||||
use crate::pkcs12::{Pbkdf1Usage, Pkcs12CryptoContext, Pkcs12Error, Pkcs12HashAlgorithm, pbkdf1};
|
||||
use hmac::KeyInit;
|
||||
use picky_asn1::wrapper::OctetStringAsn1;
|
||||
use picky_asn1_x509::pkcs12::{MacData as MacDataAsn1, Pkcs12DigestInfo as Pkcs12DigestInfoAsn1};
|
||||
use thiserror::Error;
|
||||
|
||||
const DEFAULT_MAC_KDF_ITERATIONS: u32 = 1;
|
||||
const DEFAULT_SALT_SIZE: usize = 20;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum Pkcs12MacError {
|
||||
#[error("Invalid hmac input size")]
|
||||
InvalidHmacInputSize,
|
||||
#[error("MAC validation failed (wrong password or corrupted data)")]
|
||||
MacValidation,
|
||||
}
|
||||
|
||||
/// HMAC algorithm parameters (used for PFX integrity data)
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Pkcs12MacAlgorithmHmac {
|
||||
hash_algorithm: Pkcs12HashAlgorithm,
|
||||
iterations: Option<u32>,
|
||||
}
|
||||
|
||||
impl Pkcs12MacAlgorithmHmac {
|
||||
pub fn new(hash_algorithm: Pkcs12HashAlgorithm) -> Self {
|
||||
Self {
|
||||
hash_algorithm,
|
||||
iterations: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_iterations(mut self, iterations: u32) -> Self {
|
||||
self.iterations = Some(iterations);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn hash_algorithm(&self) -> Pkcs12HashAlgorithm {
|
||||
self.hash_algorithm
|
||||
}
|
||||
|
||||
pub fn iterations(&self) -> Option<u32> {
|
||||
self.iterations
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed MAC algorithm parameters
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Pkcs12MacAlgorithm {
|
||||
Hmac(Pkcs12MacAlgorithmHmac),
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Parsed PFX MAC data
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Pkcs12MacData {
|
||||
algorithm: Pkcs12MacAlgorithm,
|
||||
inner: MacDataAsn1,
|
||||
}
|
||||
|
||||
impl Pkcs12MacData {
|
||||
pub(crate) fn from_asn1(inner: MacDataAsn1, skip_unknown_hash_algorithm: bool) -> Result<Self, Pkcs12Error> {
|
||||
let hash_algorithm = match Pkcs12HashAlgorithm::from_asn1_digest_algorithm(&inner.mac.digest_algorithm) {
|
||||
Ok(algorithm) => algorithm,
|
||||
Err(_) if skip_unknown_hash_algorithm => {
|
||||
return Ok(Self {
|
||||
algorithm: Pkcs12MacAlgorithm::Unknown,
|
||||
inner,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let kdf_iterations = inner.iterations.unwrap_or(DEFAULT_MAC_KDF_ITERATIONS);
|
||||
|
||||
Ok(Self {
|
||||
algorithm: Pkcs12MacAlgorithm::Hmac(Pkcs12MacAlgorithmHmac {
|
||||
hash_algorithm,
|
||||
iterations: Some(kdf_iterations),
|
||||
}),
|
||||
inner,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn new_hmac(
|
||||
algorithm: Pkcs12MacAlgorithmHmac,
|
||||
context: &mut Pkcs12CryptoContext,
|
||||
data: &[u8],
|
||||
) -> Result<Self, Pkcs12Error> {
|
||||
let hash_algorithm = algorithm.hash_algorithm;
|
||||
let kdf_iterations = algorithm.iterations.unwrap_or(DEFAULT_MAC_KDF_ITERATIONS);
|
||||
let salt = context.generate_bytes(DEFAULT_SALT_SIZE);
|
||||
|
||||
// PKCS12 MAC uses BMPString as password representation
|
||||
let password = context.password_bytes_pbes1()?;
|
||||
|
||||
let digest = Self::calculate_digest(
|
||||
hash_algorithm,
|
||||
kdf_iterations,
|
||||
password.as_slice(),
|
||||
salt.as_slice(),
|
||||
data,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
algorithm: Pkcs12MacAlgorithm::Hmac(algorithm),
|
||||
inner: MacDataAsn1 {
|
||||
mac: Pkcs12DigestInfoAsn1 {
|
||||
digest_algorithm: hash_algorithm.into(),
|
||||
digest: OctetStringAsn1(digest),
|
||||
},
|
||||
salt: OctetStringAsn1(salt),
|
||||
iterations: Some(kdf_iterations),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn validate(&self, context: &Pkcs12CryptoContext, data: &[u8]) -> Result<(), Pkcs12Error> {
|
||||
let hash_algorithm = Pkcs12HashAlgorithm::from_asn1_digest_algorithm(&self.inner.mac.digest_algorithm)?;
|
||||
let kdf_iterations = self.inner.iterations.unwrap_or(DEFAULT_MAC_KDF_ITERATIONS);
|
||||
let salt = self.inner.salt.0.as_slice();
|
||||
|
||||
// PKCS12 MAC uses BMPString as password representation
|
||||
let password = context.password_bytes_pbes1()?;
|
||||
|
||||
let digest = Self::calculate_digest(hash_algorithm, kdf_iterations, password.as_slice(), salt, data)?;
|
||||
|
||||
if digest == self.inner.mac.digest.0.as_slice() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Pkcs12MacError::MacValidation.into())
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_digest(
|
||||
hash_algorithm: Pkcs12HashAlgorithm,
|
||||
kdf_iterations: u32,
|
||||
password: &[u8],
|
||||
salt: &[u8],
|
||||
data: &[u8],
|
||||
) -> Result<Vec<u8>, Pkcs12Error> {
|
||||
let key = pbkdf1(
|
||||
hash_algorithm,
|
||||
password,
|
||||
salt,
|
||||
kdf_iterations as usize,
|
||||
Pbkdf1Usage::Mac,
|
||||
hash_algorithm.digest_size(),
|
||||
);
|
||||
|
||||
use hmac::Mac;
|
||||
|
||||
let map_hmac_err = |_| Pkcs12MacError::InvalidHmacInputSize;
|
||||
|
||||
let mac = match hash_algorithm {
|
||||
Pkcs12HashAlgorithm::Sha1 => {
|
||||
let mut hmac = hmac::Hmac::<sha1::Sha1>::new_from_slice(&key).map_err(map_hmac_err)?;
|
||||
hmac.update(data);
|
||||
hmac.finalize().into_bytes().to_vec()
|
||||
}
|
||||
Pkcs12HashAlgorithm::Sha224 => {
|
||||
let mut hmac = hmac::Hmac::<sha2::Sha224>::new_from_slice(&key).map_err(map_hmac_err)?;
|
||||
hmac.update(data);
|
||||
hmac.finalize().into_bytes().to_vec()
|
||||
}
|
||||
Pkcs12HashAlgorithm::Sha256 => {
|
||||
let mut hmac = hmac::Hmac::<sha2::Sha256>::new_from_slice(&key).map_err(map_hmac_err)?;
|
||||
hmac.update(data);
|
||||
hmac.finalize().into_bytes().to_vec()
|
||||
}
|
||||
Pkcs12HashAlgorithm::Sha384 => {
|
||||
let mut hmac = hmac::Hmac::<sha2::Sha384>::new_from_slice(&key).map_err(map_hmac_err)?;
|
||||
hmac.update(data);
|
||||
hmac.finalize().into_bytes().to_vec()
|
||||
}
|
||||
Pkcs12HashAlgorithm::Sha512 => {
|
||||
let mut hmac = hmac::Hmac::<sha2::Sha512>::new_from_slice(&key).map_err(map_hmac_err)?;
|
||||
hmac.update(data);
|
||||
hmac.finalize().into_bytes().to_vec()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(mac)
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &MacDataAsn1 {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
/// Parsed MAC algorithm parameters
|
||||
pub fn algorithm(&self) -> &Pkcs12MacAlgorithm {
|
||||
&self.algorithm
|
||||
}
|
||||
}
|
||||
Vendored
+614
@@ -0,0 +1,614 @@
|
||||
//! This module provides types for parsing and building PKCS#12 files (PFX).
|
||||
//!
|
||||
//! # PFX file structure
|
||||
//!
|
||||
//! PFX files are constructed from `safe contents` on the highest level, which are containers for
|
||||
//! various types of `safe bags`. They could be encrypted or not and contain one or more `safe bags`.
|
||||
//!
|
||||
//! `Safe bags` are polymorphic containers for various types of data, such as private keys, certificates,
|
||||
//! custom data (`secrets`), CRLs, etc. They could also be nested, which means that one `safe bag` could
|
||||
//! contain multiple `safe bags` inside, making it possible to represent complex tree-like structures.
|
||||
//!
|
||||
//! Each `safe bag` could also contain `attributes`, which are key-value pairs of various types of data.
|
||||
//! High-level API defines only `friendly name` and `local key id` attributes, but it is possible to
|
||||
//! parse/build any custom attributes as well.
|
||||
//!
|
||||
//! There are also special `safe bags` called `shrouded key bags`, which are basically encrypted
|
||||
//! PKCS8 private keys. Encryption on `safe bag` level is performed to allow PFX files that have
|
||||
//! unencrypted certificates and encrypted private keys, to be preprocessed by the software without
|
||||
//! knowing PFX password (e.g. check if PFX contains private key for the certificate or not). Usually
|
||||
//! relations between contained certificates and private keys are marked with `local key id` attribute
|
||||
//! which is set to the same value for both certificate and private key.
|
||||
//!
|
||||
//! # Parsing
|
||||
//!
|
||||
//! Almost any kind of PKCS#12 object specified by [RFC](https://datatracker.ietf.org/doc/html/rfc7292)
|
||||
//! could be parsed or constructed. However, only the most used PFX types are wrapped in high level
|
||||
//! convenient API such as private keys, certificates, nested pfx nodes, etc. If parsed
|
||||
//! data was in fact some rarely used PFX type, it is still possible to access it via low level API
|
||||
//! via `.inner()` method usually returns ASN.1 DER encoded data (`picky-asn1-x509` crate types).
|
||||
//!
|
||||
//! Parsing process could be controlled even more with [`Pkcs12ParsingParams`], which allows to skip
|
||||
//! some parsing errors (e.g. Skip failed mapping of private key ASN.1 structure to high-level picky
|
||||
//! wrapper)
|
||||
//!
|
||||
//! Parsing of PFX file is simple as calling [`Pfx::from_der`] method with required params
|
||||
//! (crypto context which holds PFX password, parsing params and data itself), after which its safe
|
||||
//! bags could be inspected and data extracted from PFX.
|
||||
//!
|
||||
//! # Building
|
||||
//!
|
||||
//! Building process of PFX files is down-to-top, in contrast to parsing. It starts with
|
||||
//! building of required safe bags/attributes structures, wrapping them inside safe contents
|
||||
//! (encrypted or unencrypted) and then wrapping them in PFX structure. This allows to keep API
|
||||
//! flexible while representing Pfx file both after parsing and building as the same type ([`Pfx])
|
||||
//!
|
||||
//! # Encryption
|
||||
//!
|
||||
//! - It is advised to always use Pbes2 AES-based encryption for PFX files. The only use case for new
|
||||
//! PFX files with Pbes1 encryption is to support legacy software that does not support Pbes2.
|
||||
//! - Usually PFX files without passwords are actually encrypted with empty-string passwords if
|
||||
//! generated by modern software by default. (e.g. certmgr or OpenSSL). However, it is possible to
|
||||
//! create PFX files without any encryption and MAC in picky if PFX will be used just as a plain
|
||||
//! container for certificates and private keys and stored somewhere securely (e.g. wrapped in
|
||||
//! another encryption layer).
|
||||
|
||||
mod attribute;
|
||||
mod encryption;
|
||||
mod mac;
|
||||
mod pbkdf1;
|
||||
mod safe_bag;
|
||||
mod safe_contents;
|
||||
|
||||
use picky_asn1::restricted_string::CharSetError;
|
||||
use picky_asn1_der::Asn1RawDer;
|
||||
use picky_asn1_x509::oid::ObjectIdentifier;
|
||||
use picky_asn1_x509::pkcs12::{
|
||||
AuthenticatedSafeContentInfo as AuthenticatedSafeContentInfoAsn1,
|
||||
ParsedAuthenticatedSafeDataRepr as ParsedAuthenticatedSafeDataReprAsn1, Pbkdf2Prf as Pbkdf2PrfAsn1, Pfx as PfxAsn1,
|
||||
Pkcs12DigestAlgorithm as Pkcs12DigestAlgorithmAsn1, Pkcs12DigestAlgorithm,
|
||||
RawAuthenticatedSafeContentInfo as RawAuthenticatedSafeContentInfoAsn1, RawPfx as RawPfxAsn1,
|
||||
SafeContentsContentInfo as SafeContentsContentInfoAsn1,
|
||||
};
|
||||
use std::fmt::Display;
|
||||
use thiserror::Error;
|
||||
|
||||
pub(crate) use pbkdf1::{Pbkdf1Usage, pbkdf1};
|
||||
|
||||
pub use attribute::{CustomPkcs12Attribute, Pkcs12Attribute, Pkcs12AttributeKind};
|
||||
pub use encryption::{
|
||||
Pbes1Cipher, Pbes1Encryption, Pbes2Cipher, Pbes2Encryption, Pkcs12CryptoContext, Pkcs12Encryption,
|
||||
Pkcs12EncryptionKind,
|
||||
};
|
||||
pub use mac::{Pkcs12MacAlgorithm, Pkcs12MacAlgorithmHmac, Pkcs12MacData, Pkcs12MacError};
|
||||
pub use safe_bag::{SafeBag, SafeBagKind, SecretSafeBag};
|
||||
pub use safe_contents::{SafeContents, SafeContentsKind};
|
||||
|
||||
const PFX_VERSION: u8 = 3;
|
||||
|
||||
/// Parsed PFX (PKCS12 archive). See module docs for more info on PFX file structure and API usage.
|
||||
#[derive(Debug)]
|
||||
pub struct Pfx {
|
||||
safe_contents: Vec<SafeContents>,
|
||||
mac_data: Option<Pkcs12MacData>,
|
||||
/// Pre-serialized auth safe data. Just an optimization to avoid re-serializing the auth safe
|
||||
/// data if it was already serialized for the MAC calculation.
|
||||
auth_safe_data: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Pfx {
|
||||
/// Create new PFX file with HMAC MAC algorithm. Usually this is the default in modern software
|
||||
pub fn new_with_hmac(
|
||||
safe_contents: Vec<SafeContents>,
|
||||
mac: Pkcs12MacAlgorithmHmac,
|
||||
crypto_context: &mut Pkcs12CryptoContext,
|
||||
) -> Result<Self, Pkcs12Error> {
|
||||
let safe_contents_asn1 = safe_contents.iter().map(|sc| sc.inner().clone()).collect::<Vec<_>>();
|
||||
|
||||
let serialized_auth_safe = picky_asn1_der::to_vec(&safe_contents_asn1)?;
|
||||
let mac_data = Pkcs12MacData::new_hmac(mac, crypto_context, &serialized_auth_safe)?;
|
||||
|
||||
Ok(Self {
|
||||
safe_contents,
|
||||
mac_data: Some(mac_data),
|
||||
auth_safe_data: Some(serialized_auth_safe),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create new PFX file without MAC algorithm if needed for some reason (e.g. MAC is performed
|
||||
/// on the higher level)
|
||||
pub fn new_without_mac(safe_contents: Vec<SafeContents>) -> Self {
|
||||
Self {
|
||||
safe_contents,
|
||||
mac_data: None,
|
||||
auth_safe_data: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize PFX file to DER bytes
|
||||
pub fn to_der(&self) -> Result<Vec<u8>, Pkcs12Error> {
|
||||
match &self.auth_safe_data {
|
||||
Some(auth_safe) => {
|
||||
let pfx_asn1 = RawPfxAsn1 {
|
||||
version: PFX_VERSION,
|
||||
auth_safe: RawAuthenticatedSafeContentInfoAsn1::Data(Asn1RawDer(auth_safe.clone())),
|
||||
mac_data: self.mac_data.as_ref().map(|mac_data| mac_data.inner().clone()),
|
||||
};
|
||||
|
||||
picky_asn1_der::to_vec(&pfx_asn1).map_err(Into::into)
|
||||
}
|
||||
None => {
|
||||
let pfx_asn1 = PfxAsn1 {
|
||||
version: PFX_VERSION,
|
||||
auth_safe: AuthenticatedSafeContentInfoAsn1::<ParsedAuthenticatedSafeDataReprAsn1>::Data(
|
||||
self.safe_contents
|
||||
.iter()
|
||||
.map(|sc| sc.inner().clone())
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
mac_data: self.mac_data.as_ref().map(|mac_data| mac_data.inner().clone()),
|
||||
};
|
||||
|
||||
picky_asn1_der::to_vec(&pfx_asn1).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a PKCS12 archive (PFX) from its DER representation.
|
||||
pub fn from_der(
|
||||
data: &[u8],
|
||||
crypto_context: &Pkcs12CryptoContext,
|
||||
parsing_params: &Pkcs12ParsingParams,
|
||||
) -> Result<Self, Pkcs12Error> {
|
||||
let (auth_safe, mac_data) = if parsing_params.skip_mac_validation {
|
||||
let pfx_asn1: PfxAsn1 = picky_asn1_der::from_bytes(data)?;
|
||||
if pfx_asn1.version != PFX_VERSION {
|
||||
return Err(Pkcs12Error::InvalidVersion(pfx_asn1.version));
|
||||
}
|
||||
let mac_data = pfx_asn1
|
||||
.mac_data
|
||||
.map(|asn1| Pkcs12MacData::from_asn1(asn1, true))
|
||||
.transpose()?;
|
||||
|
||||
let auth_safe = match pfx_asn1.auth_safe {
|
||||
AuthenticatedSafeContentInfoAsn1::Data(data) => data,
|
||||
AuthenticatedSafeContentInfoAsn1::Unknown { content_type, .. } => {
|
||||
return Err(Pkcs12Error::InvalidAuthenticatedSafeContentType(content_type.into()));
|
||||
}
|
||||
};
|
||||
|
||||
(auth_safe, mac_data)
|
||||
} else {
|
||||
let pfx_asn1: RawPfxAsn1 = picky_asn1_der::from_bytes(data)?;
|
||||
if pfx_asn1.version != PFX_VERSION {
|
||||
return Err(Pkcs12Error::InvalidVersion(pfx_asn1.version));
|
||||
}
|
||||
|
||||
let mac_data = pfx_asn1
|
||||
.mac_data
|
||||
.map(|asn1| Pkcs12MacData::from_asn1(asn1, false))
|
||||
.transpose()?;
|
||||
|
||||
let auth_safe = match pfx_asn1.auth_safe {
|
||||
AuthenticatedSafeContentInfoAsn1::Data(data) => {
|
||||
if let Some(mac_data) = &mac_data {
|
||||
mac_data.validate(crypto_context, data.0.as_slice())?;
|
||||
}
|
||||
|
||||
let parsed: Vec<SafeContentsContentInfoAsn1> = picky_asn1_der::from_bytes(data.0.as_slice())?;
|
||||
parsed
|
||||
}
|
||||
AuthenticatedSafeContentInfoAsn1::Unknown { content_type, .. } => {
|
||||
return Err(Pkcs12Error::InvalidAuthenticatedSafeContentType(content_type.into()));
|
||||
}
|
||||
};
|
||||
|
||||
(auth_safe, mac_data)
|
||||
};
|
||||
|
||||
auth_safe
|
||||
.into_iter()
|
||||
.map(|safe_contents| SafeContents::from_asn1(safe_contents, crypto_context, parsing_params))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map(|safe_contents| Self {
|
||||
safe_contents,
|
||||
mac_data,
|
||||
auth_safe_data: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Inspect parsed PFX data
|
||||
pub fn safe_contents(&self) -> &[SafeContents] {
|
||||
&self.safe_contents
|
||||
}
|
||||
|
||||
/// Inspect parsed MAC data
|
||||
pub fn mac_data(&self) -> Option<&Pkcs12MacData> {
|
||||
self.mac_data.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters which control some aspects of PFX file parsing process
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Pkcs12ParsingParams {
|
||||
/// Continue parsing if conversion to high level picky data structure fails (e.g. due to
|
||||
/// unsupported private key or certificate kind)
|
||||
pub skip_soft_parsing_errors: bool,
|
||||
|
||||
/// Continue parsing if decryption fails and keep data in encrypted form
|
||||
pub skip_decryption_errors: bool,
|
||||
|
||||
/// Continue parsing if MAC validation fails.
|
||||
///
|
||||
/// This is useful for parsing available unencrypted data from
|
||||
/// password-protected PFX files. Also could be useful if PFX integrity has been intentionally
|
||||
/// violated for testing purposes.
|
||||
pub skip_mac_validation: bool,
|
||||
}
|
||||
|
||||
/// Hashing algorithm used for MAC or KDF in PFX file
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Pkcs12HashAlgorithm {
|
||||
Sha1,
|
||||
Sha224,
|
||||
Sha256,
|
||||
Sha384,
|
||||
Sha512,
|
||||
}
|
||||
|
||||
impl Pkcs12HashAlgorithm {
|
||||
pub(crate) fn pbkdf1_u_bits(self) -> usize {
|
||||
match self {
|
||||
Self::Sha1 => 160,
|
||||
Self::Sha224 => 224,
|
||||
Self::Sha256 => 256,
|
||||
Self::Sha384 => 384,
|
||||
Self::Sha512 => 512,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pbkdf1_v_bits(self) -> usize {
|
||||
match self {
|
||||
Self::Sha1 => 512,
|
||||
Self::Sha224 => 512,
|
||||
Self::Sha256 => 512,
|
||||
Self::Sha384 => 1024,
|
||||
Self::Sha512 => 1024,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn digest_size(self) -> usize {
|
||||
match self {
|
||||
Self::Sha1 => 20,
|
||||
Self::Sha224 => 28,
|
||||
Self::Sha256 => 32,
|
||||
Self::Sha384 => 48,
|
||||
Self::Sha512 => 64,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_asn1_pbkdf2_prf(value: &Pbkdf2PrfAsn1) -> Result<Self, Pkcs12Error> {
|
||||
let algorithm = match value {
|
||||
Pbkdf2PrfAsn1::HmacWithSha1 => Pkcs12HashAlgorithm::Sha1,
|
||||
Pbkdf2PrfAsn1::HmacWithSha224 => Pkcs12HashAlgorithm::Sha224,
|
||||
Pbkdf2PrfAsn1::HmacWithSha256 => Pkcs12HashAlgorithm::Sha256,
|
||||
Pbkdf2PrfAsn1::HmacWithSha384 => Pkcs12HashAlgorithm::Sha384,
|
||||
Pbkdf2PrfAsn1::HmacWithSha512 => Pkcs12HashAlgorithm::Sha512,
|
||||
Pbkdf2PrfAsn1::Unknown(raw) => {
|
||||
let oid = raw.algorithm().clone();
|
||||
return Err(Pkcs12Error::NotSupportedAlgorithm {
|
||||
algorithm: UnsupportedPkcs12Algorithm::Oid(oid),
|
||||
context: "Crypto operation (pbkdf2 prf algorithm)".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Ok(algorithm)
|
||||
}
|
||||
|
||||
pub(crate) fn from_asn1_digest_algorithm(value: &Pkcs12DigestAlgorithmAsn1) -> Result<Self, Pkcs12Error> {
|
||||
let algorithm = match value {
|
||||
Pkcs12DigestAlgorithm::Sha1 => Pkcs12HashAlgorithm::Sha1,
|
||||
Pkcs12DigestAlgorithm::Sha224 => Pkcs12HashAlgorithm::Sha224,
|
||||
Pkcs12DigestAlgorithm::Sha256 => Pkcs12HashAlgorithm::Sha256,
|
||||
Pkcs12DigestAlgorithm::Sha384 => Pkcs12HashAlgorithm::Sha384,
|
||||
Pkcs12DigestAlgorithm::Sha512 => Pkcs12HashAlgorithm::Sha512,
|
||||
Pkcs12DigestAlgorithm::Unknown(raw) => {
|
||||
let oid = raw.algorithm().clone();
|
||||
return Err(Pkcs12Error::NotSupportedAlgorithm {
|
||||
algorithm: UnsupportedPkcs12Algorithm::Oid(oid),
|
||||
context: "MAC calculation (pbkdf1 prf algorithm)".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Ok(algorithm)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pkcs12HashAlgorithm> for Pkcs12DigestAlgorithm {
|
||||
fn from(value: Pkcs12HashAlgorithm) -> Self {
|
||||
match value {
|
||||
Pkcs12HashAlgorithm::Sha1 => Self::Sha1,
|
||||
Pkcs12HashAlgorithm::Sha224 => Self::Sha224,
|
||||
Pkcs12HashAlgorithm::Sha256 => Self::Sha256,
|
||||
Pkcs12HashAlgorithm::Sha384 => Self::Sha384,
|
||||
Pkcs12HashAlgorithm::Sha512 => Self::Sha512,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Pkcs12Error {
|
||||
#[error("Not supported algorithm `{algorithm}` in context of {context}")]
|
||||
NotSupportedAlgorithm {
|
||||
algorithm: UnsupportedPkcs12Algorithm,
|
||||
context: String,
|
||||
},
|
||||
#[error("Failed to perform PBES1 crypto operation: {context}")]
|
||||
Pbes1 { context: String },
|
||||
#[error("Failed to perform PBES2 crypto operation: {context}")]
|
||||
Pbes2 { context: String },
|
||||
#[error(transparent)]
|
||||
CharSet(#[from] CharSetError),
|
||||
#[error(transparent)]
|
||||
Mac(#[from] mac::Pkcs12MacError),
|
||||
#[error("Invalid ASN.1 DER encoding")]
|
||||
Asn1Der(#[from] picky_asn1_der::Asn1DerError),
|
||||
#[error(transparent)]
|
||||
Key(#[from] crate::key::KeyError),
|
||||
#[error(transparent)]
|
||||
Certificate(#[from] crate::x509::certificate::CertError),
|
||||
#[error(transparent)]
|
||||
RandError(#[from] rand::rngs::SysError),
|
||||
#[error("Not supported or invalid PFX version: {0}")]
|
||||
InvalidVersion(u8),
|
||||
#[error("Invalid PFX AuthenticatedSafe content type: {0}")]
|
||||
InvalidAuthenticatedSafeContentType(UnsupportedPkcs12Algorithm),
|
||||
#[error("Unexpected attribute values count. Expected: `{expected}`, got: `{actual}`")]
|
||||
UnexpectedAttributeValuesCount { expected: usize, actual: usize },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum UnsupportedPkcs12Algorithm {
|
||||
Named(&'static str),
|
||||
Oid(ObjectIdentifier),
|
||||
}
|
||||
|
||||
impl From<&'static str> for UnsupportedPkcs12Algorithm {
|
||||
fn from(name: &'static str) -> Self {
|
||||
Self::Named(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjectIdentifier> for UnsupportedPkcs12Algorithm {
|
||||
fn from(oid: ObjectIdentifier) -> Self {
|
||||
Self::Oid(oid)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for UnsupportedPkcs12Algorithm {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
UnsupportedPkcs12Algorithm::Named(name) => f.write_str(name),
|
||||
UnsupportedPkcs12Algorithm::Oid(oid) => {
|
||||
write!(f, "OID({})", format_oid(oid))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pkcs12Error {
|
||||
pub fn unsupported_algorithm(algorithm: impl Into<UnsupportedPkcs12Algorithm>, context: impl Into<String>) -> Self {
|
||||
Self::NotSupportedAlgorithm {
|
||||
algorithm: algorithm.into(),
|
||||
context: context.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_oid(oid: &ObjectIdentifier) -> String {
|
||||
let oid_str: String = oid.clone().into();
|
||||
format!("OID({oid_str})")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::key::PrivateKey;
|
||||
use crate::pem::Pem;
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn pfx_certmgr_aes256() {
|
||||
let encoded = picky_test_data::CERTMGR_AES256;
|
||||
let crypto_context = Pkcs12CryptoContext::new_with_password("test").unwrap();
|
||||
let _decoded = Pfx::from_der(encoded, &crypto_context, &Pkcs12ParsingParams::default()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pfx_certmgr_3des() {
|
||||
let encoded = picky_test_data::CERTMGR_3DES;
|
||||
let crypto_context = Pkcs12CryptoContext::new_with_password("test").unwrap();
|
||||
let _decoded = Pfx::from_der(encoded, &crypto_context, &Pkcs12ParsingParams::default()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pfx_certmgr_rc2() {
|
||||
let encoded = picky_test_data::LEAF_PASSWORD_IS_ABC;
|
||||
let crypto_context = Pkcs12CryptoContext::new_with_password("abc").unwrap();
|
||||
let _decoded = Pfx::from_der(encoded, &crypto_context, &Pkcs12ParsingParams::default()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pfx_certmgr_rc2_empty_pass() {
|
||||
let encoded = picky_test_data::LEAF_EMPTY_PASSWORD;
|
||||
let crypto_context = Pkcs12CryptoContext::new_without_password().unwrap();
|
||||
let _decoded = Pfx::from_der(encoded, &crypto_context, &Pkcs12ParsingParams::default()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pfx_openssl_aes_empty_pass() {
|
||||
let encoded = picky_test_data::OPENSSL_NOCRYPT;
|
||||
let crypto_context = Pkcs12CryptoContext::new_without_password().unwrap();
|
||||
let _decoded = Pfx::from_der(encoded, &crypto_context, &Pkcs12ParsingParams::default()).unwrap();
|
||||
}
|
||||
|
||||
fn stable_rand() -> impl rand_core::CryptoRng {
|
||||
use rand_core::SeedableRng as _;
|
||||
rand_chacha::ChaChaRng::seed_from_u64(42)
|
||||
}
|
||||
|
||||
fn build_cert_bags() -> [SafeBag; 3] {
|
||||
let leaf = crate::x509::Cert::from_der(picky_test_data::ASSERT_LEAF).unwrap();
|
||||
let intermediate = crate::x509::Cert::from_der(picky_test_data::ASSERT_INTERMEDIATE).unwrap();
|
||||
let root = crate::x509::Cert::from_der(picky_test_data::ASSERT_ROOT).unwrap();
|
||||
|
||||
let leaf_cert_bag = SafeBag::new_certificate(leaf, build_leaf_attributes()).unwrap();
|
||||
|
||||
let intermediate_cert_bag = SafeBag::new_certificate(
|
||||
intermediate,
|
||||
vec![Pkcs12Attribute::new_friendly_name(
|
||||
"PICKY_INTERMEDIATE".parse().unwrap(),
|
||||
)],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let root_cert_bag = SafeBag::new_certificate(
|
||||
root,
|
||||
vec![Pkcs12Attribute::new_friendly_name("PICKY_ROOT".parse().unwrap())],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
[leaf_cert_bag, intermediate_cert_bag, root_cert_bag]
|
||||
}
|
||||
|
||||
fn build_leaf_attributes() -> Vec<Pkcs12Attribute> {
|
||||
vec![
|
||||
Pkcs12Attribute::new_local_key_id([0x01, 0x00, 0x00, 0x00]),
|
||||
Pkcs12Attribute::new_friendly_name("PICKY_LEAF".parse().unwrap()),
|
||||
]
|
||||
}
|
||||
|
||||
fn make_crypto_context(password: Option<&str>) -> Pkcs12CryptoContext {
|
||||
if let Some(password) = password {
|
||||
Pkcs12CryptoContext::new_with_password(password)
|
||||
.unwrap()
|
||||
.with_rng(stable_rand())
|
||||
} else {
|
||||
Pkcs12CryptoContext::new_without_password()
|
||||
.unwrap()
|
||||
.with_rng(stable_rand())
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_pfx(der_data: &[u8], password: Option<&str>) {
|
||||
let crypto_context = make_crypto_context(password);
|
||||
|
||||
// Check that we can decode PFX encoded by picky itself
|
||||
let _decoded = Pfx::from_der(der_data, &crypto_context, &Pkcs12ParsingParams::default()).unwrap();
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let temp_path = tempfile::NamedTempFile::new().unwrap().into_temp_path();
|
||||
|
||||
std::fs::write(&temp_path, der_data).unwrap();
|
||||
|
||||
let certutil_args = vec![
|
||||
"-dump".to_string(),
|
||||
"-p".to_string(),
|
||||
password.unwrap_or("").to_string(),
|
||||
temp_path.to_str().unwrap().to_string(),
|
||||
];
|
||||
|
||||
let certutil_output = std::process::Command::new("certutil")
|
||||
.args(certutil_args)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
certutil_output.status.success(),
|
||||
"certutil failed: {}",
|
||||
String::from_utf8_lossy(&certutil_output.stdout)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn encryption_3des(crypto_context: &mut Pkcs12CryptoContext) -> Pkcs12Encryption {
|
||||
Pkcs12Encryption::new_pbes1(Pbes1Encryption::new(Pbes1Cipher::ShaAnd3Key3DesCbc), crypto_context)
|
||||
}
|
||||
|
||||
fn encryption_rc2(crypto_context: &mut Pkcs12CryptoContext) -> Pkcs12Encryption {
|
||||
Pkcs12Encryption::new_pbes1(Pbes1Encryption::new(Pbes1Cipher::ShaAnd40BitRc2Cbc), crypto_context)
|
||||
}
|
||||
|
||||
fn encryption_aes256(crypto_context: &mut Pkcs12CryptoContext) -> Pkcs12Encryption {
|
||||
Pkcs12Encryption::new_pbes2(
|
||||
Pbes2Encryption::new(Pbes2Cipher::Aes256Cbc, Pkcs12HashAlgorithm::Sha256),
|
||||
crypto_context,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn leaf_private_key_rsa() -> PrivateKey {
|
||||
let pem = picky_test_data::RSA_2048_PK_3.parse::<Pem>().unwrap();
|
||||
PrivateKey::from_pkcs8(pem.data()).unwrap()
|
||||
}
|
||||
|
||||
type EncryptionFn = fn(&mut Pkcs12CryptoContext) -> Pkcs12Encryption;
|
||||
|
||||
#[rstest]
|
||||
#[case(encryption_3des, Some("test"), Pkcs12HashAlgorithm::Sha1)]
|
||||
#[case(encryption_rc2, Some("test"), Pkcs12HashAlgorithm::Sha1)]
|
||||
#[case(encryption_aes256, Some("test"), Pkcs12HashAlgorithm::Sha256)]
|
||||
#[case(encryption_3des, None, Pkcs12HashAlgorithm::Sha1)]
|
||||
// RC2 uses same KDF as 3DES so we could skip case of RC2 without password
|
||||
#[case(encryption_aes256, None, Pkcs12HashAlgorithm::Sha256)]
|
||||
fn build_pfx_encrypted(
|
||||
#[case] encryption_fn: EncryptionFn,
|
||||
#[case] password: Option<&'static str>,
|
||||
#[case] hmac_algorithm: Pkcs12HashAlgorithm,
|
||||
) {
|
||||
let leaf_key = leaf_private_key_rsa();
|
||||
|
||||
let [leaf_cert_bag, intermediate_cert_bag, root_cert_bag] = build_cert_bags();
|
||||
|
||||
let mut crypto_context = make_crypto_context(password);
|
||||
|
||||
let key_encryption = encryption_fn(&mut crypto_context);
|
||||
|
||||
let leaf_key_bag =
|
||||
SafeBag::new_encrypted_key(leaf_key, build_leaf_attributes(), key_encryption, &crypto_context).unwrap();
|
||||
|
||||
let cert_encryption = encryption_fn(&mut crypto_context);
|
||||
|
||||
let cert_safe_contents = SafeContents::new_encrypted(
|
||||
vec![leaf_cert_bag, intermediate_cert_bag, root_cert_bag],
|
||||
cert_encryption,
|
||||
&crypto_context,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let key_safe_contents = SafeContents::new(vec![leaf_key_bag]);
|
||||
|
||||
let secret_safe_bag = SafeBag::new_secret(
|
||||
SecretSafeBag::new(picky_asn1_x509::oids::content_info_type_data(), &42u8).unwrap(),
|
||||
vec![Pkcs12Attribute::new_custom(
|
||||
CustomPkcs12Attribute::new_single_value(picky_asn1_x509::oids::content_info_type_data(), &256u64)
|
||||
.unwrap(),
|
||||
)],
|
||||
);
|
||||
|
||||
let secret_safe_contents = SafeContents::new(vec![secret_safe_bag]);
|
||||
|
||||
let pfx = Pfx::new_with_hmac(
|
||||
vec![cert_safe_contents, key_safe_contents, secret_safe_contents],
|
||||
Pkcs12MacAlgorithmHmac::new(hmac_algorithm),
|
||||
&mut crypto_context,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let der_data = pfx.to_der().unwrap();
|
||||
|
||||
validate_pfx(&der_data, password);
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
use crate::pkcs12::Pkcs12HashAlgorithm;
|
||||
|
||||
pub enum Pbkdf1Usage {
|
||||
Key,
|
||||
Iv,
|
||||
Mac,
|
||||
}
|
||||
|
||||
impl Pbkdf1Usage {
|
||||
fn to_id_byte(&self) -> u8 {
|
||||
match self {
|
||||
Pbkdf1Usage::Key => 1,
|
||||
Pbkdf1Usage::Iv => 2,
|
||||
Pbkdf1Usage::Mac => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// PBKDF1 implementation for PKCS#12 as defined in [RFC](https://datatracker.ietf.org/doc/html/rfc7292#appendix-B.2)
|
||||
pub fn pbkdf1(
|
||||
hash: Pkcs12HashAlgorithm,
|
||||
password: &[u8],
|
||||
salt: &[u8],
|
||||
kdf_iterations: usize,
|
||||
usage: Pbkdf1Usage,
|
||||
output_size: usize,
|
||||
) -> Vec<u8> {
|
||||
let u = hash.pbkdf1_u_bits() / 8;
|
||||
let v = hash.pbkdf1_v_bits() / 8;
|
||||
|
||||
let hash_round = match hash {
|
||||
Pkcs12HashAlgorithm::Sha1 => pbkdf1_hash_round::<sha1::Sha1>,
|
||||
Pkcs12HashAlgorithm::Sha224 => pbkdf1_hash_round::<sha2::Sha224>,
|
||||
Pkcs12HashAlgorithm::Sha256 => pbkdf1_hash_round::<sha2::Sha256>,
|
||||
Pkcs12HashAlgorithm::Sha384 => pbkdf1_hash_round::<sha2::Sha384>,
|
||||
Pkcs12HashAlgorithm::Sha512 => pbkdf1_hash_round::<sha2::Sha512>,
|
||||
};
|
||||
|
||||
// Construct "diversifier" string
|
||||
let d = vec![usage.to_id_byte(); v];
|
||||
|
||||
let expanded_length = |len: usize| v * len.div_ceil(v);
|
||||
|
||||
// Expand salt and password length to multiple of V
|
||||
let expanded_salt = salt.iter().cycle().take(expanded_length(salt.len()));
|
||||
let expanded_password = password.iter().cycle().take(expanded_length(password.len()));
|
||||
|
||||
// I = S || P
|
||||
let mut key_material: Vec<u8> = expanded_salt.chain(expanded_password).cloned().collect();
|
||||
|
||||
let c = output_size.div_ceil(u);
|
||||
|
||||
let mut output: Vec<u8> = vec![];
|
||||
|
||||
// Temporary buffer for key blocks produced by SHA1
|
||||
let mut key_block = vec![];
|
||||
|
||||
let mut b = vec![];
|
||||
|
||||
for _ in 1..c {
|
||||
hash_round(&d, &key_material, kdf_iterations, &mut key_block);
|
||||
output.extend_from_slice(&key_block);
|
||||
|
||||
// Create concatenated string B of length V
|
||||
b.clear();
|
||||
b.extend(key_block.iter().cycle().take(v).copied());
|
||||
|
||||
// Pretty convoluted operation which is defined in RFC as follows:
|
||||
//
|
||||
// C. Treating I as a concatenation I_0, I_1, ..., I_(k-1) of v-bit
|
||||
// blocks, where k=ceiling(s/v)+ceiling(p/v), modify I by
|
||||
// setting I_j=(I_j+B+1) mod 2^v for each j.
|
||||
//
|
||||
// Implementation of this part has been borrowed from [p12 crate](https://github.com/hjiayz/p12)
|
||||
let b_iter = b.iter().rev().cycle().take(key_material.len());
|
||||
let i_b_iter = key_material.iter_mut().rev().zip(b_iter);
|
||||
let mut inc = 1u8;
|
||||
for (i3, (ii, bi)) in i_b_iter.enumerate() {
|
||||
if (i3 % v) == 0 {
|
||||
inc = 1;
|
||||
}
|
||||
let (ii2, inc2) = ii.overflowing_add(*bi);
|
||||
let (ii3, inc3) = ii2.overflowing_add(inc);
|
||||
inc = (inc2 || inc3) as u8;
|
||||
*ii = ii3;
|
||||
}
|
||||
}
|
||||
|
||||
hash_round(&d, &key_material, kdf_iterations, &mut key_block);
|
||||
output.extend_from_slice(&key_block);
|
||||
|
||||
// Truncate to output_size
|
||||
output.resize(output_size, 0);
|
||||
output
|
||||
}
|
||||
|
||||
fn pbkdf1_hash_round<H: digest::Digest + digest::FixedOutputReset>(
|
||||
d: &[u8],
|
||||
i: &[u8],
|
||||
iterations: usize,
|
||||
output_buffer: &mut Vec<u8>,
|
||||
) {
|
||||
let mut hasher = H::new();
|
||||
output_buffer.clear();
|
||||
output_buffer.extend_from_slice(d);
|
||||
output_buffer.extend_from_slice(i);
|
||||
|
||||
for _ in 0..iterations {
|
||||
digest::Digest::update(&mut hasher, &output_buffer);
|
||||
let hash = hasher.finalize_reset();
|
||||
output_buffer.clear();
|
||||
output_buffer.extend_from_slice(&hash[..]);
|
||||
}
|
||||
}
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
use crate::key::PrivateKey;
|
||||
use crate::pkcs12::{Pkcs12Attribute, Pkcs12CryptoContext, Pkcs12Encryption, Pkcs12Error, Pkcs12ParsingParams};
|
||||
use crate::x509::Cert;
|
||||
use picky_asn1::wrapper::OctetStringAsn1;
|
||||
use picky_asn1_der::Asn1RawDer;
|
||||
use picky_asn1_x509::oid::ObjectIdentifier;
|
||||
use picky_asn1_x509::pkcs12::{
|
||||
CertificateBag as CertificateBagAsn1, EncryptedKeyBag as EncryptedKeyBagAsn1,
|
||||
Pkcs12Attribute as Pkcs12AttributeAsn1, SafeBag as SafeBagAsn1, SafeBagKind as SafeBagKindAsn1,
|
||||
SafeContents as SafeContentsAsn1, SecretBag as SecretBagAsn1,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// PFX safe bag, see module docs for more information
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SafeBag {
|
||||
kind: SafeBagKind,
|
||||
attributes: Vec<Pkcs12Attribute>,
|
||||
inner: SafeBagAsn1,
|
||||
}
|
||||
|
||||
impl SafeBag {
|
||||
/// Create new safe bag holding a private key
|
||||
pub fn new_key(key: PrivateKey, attributes: Vec<Pkcs12Attribute>) -> Result<Self, Pkcs12Error> {
|
||||
// Convert to `PrivateKeyInfo` structure in DER representation
|
||||
let der_data = key.to_pkcs8()?;
|
||||
|
||||
let inner = SafeBagAsn1 {
|
||||
kind: SafeBagKindAsn1::Key(Asn1RawDer(der_data)),
|
||||
attributes: attributes_to_asn1(&attributes),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
kind: SafeBagKind::PrivateKey(key),
|
||||
attributes,
|
||||
inner,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create new safe bag with encrypted key. Note that attributes are not encrypted.
|
||||
pub fn new_encrypted_key(
|
||||
key: PrivateKey,
|
||||
attributes: Vec<Pkcs12Attribute>,
|
||||
encryption: Pkcs12Encryption,
|
||||
crypto_context: &Pkcs12CryptoContext,
|
||||
) -> Result<Self, Pkcs12Error> {
|
||||
let der_data = key.to_pkcs8()?;
|
||||
let encrypted = encryption.encrypt(&der_data, crypto_context)?;
|
||||
|
||||
let inner = SafeBagAsn1 {
|
||||
kind: SafeBagKindAsn1::EncryptedKey(EncryptedKeyBagAsn1 {
|
||||
algorithm: encryption.inner().clone(),
|
||||
encrypted_data: OctetStringAsn1(encrypted),
|
||||
}),
|
||||
attributes: attributes_to_asn1(&attributes),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
kind: SafeBagKind::EncryptedPrivateKey { encryption, key },
|
||||
attributes,
|
||||
inner,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create new safe bag with certificate
|
||||
pub fn new_certificate(cert: Cert, attributes: Vec<Pkcs12Attribute>) -> Result<Self, Pkcs12Error> {
|
||||
let der_data = cert.to_der()?;
|
||||
|
||||
let inner = SafeBagAsn1 {
|
||||
kind: SafeBagKindAsn1::Certificate(CertificateBagAsn1::X509(OctetStringAsn1(der_data))),
|
||||
attributes: attributes_to_asn1(&attributes),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
kind: SafeBagKind::Certificate(cert),
|
||||
attributes,
|
||||
inner,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates new [`SecretSafeBag`] bag
|
||||
pub fn new_secret(secret: SecretSafeBag, attributes: Vec<Pkcs12Attribute>) -> Self {
|
||||
let inner = SafeBagAsn1 {
|
||||
kind: SafeBagKindAsn1::Secret(SecretBagAsn1 {
|
||||
type_id: secret.oid.clone(),
|
||||
value: secret.data.clone(),
|
||||
}),
|
||||
attributes: attributes_to_asn1(&attributes),
|
||||
};
|
||||
|
||||
Self {
|
||||
kind: SafeBagKind::Secret(secret),
|
||||
attributes,
|
||||
inner,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates safe bag with nested safe bag list.
|
||||
pub fn new_nested(safe_bags: Vec<SafeBag>, attributes: Vec<Pkcs12Attribute>) -> Self {
|
||||
let safe_contents = SafeContentsAsn1(safe_bags.iter().map(|sb| sb.inner.clone()).collect());
|
||||
|
||||
let inner = SafeBagAsn1 {
|
||||
kind: SafeBagKindAsn1::SafeContents(safe_contents),
|
||||
attributes: attributes_to_asn1(&attributes),
|
||||
};
|
||||
|
||||
Self {
|
||||
kind: SafeBagKind::Nested(safe_bags),
|
||||
attributes,
|
||||
inner,
|
||||
}
|
||||
}
|
||||
|
||||
/// PKCS#12 allows for arbitrary SafeBags to be included in the PKCS#12 file as long as they
|
||||
/// a unique OID.
|
||||
pub fn new_custom(oid: ObjectIdentifier, value: Asn1RawDer, attributes: Vec<Pkcs12Attribute>) -> Self {
|
||||
let inner = SafeBagAsn1 {
|
||||
kind: SafeBagKindAsn1::Unknown { type_id: oid, value },
|
||||
attributes: attributes_to_asn1(&attributes),
|
||||
};
|
||||
|
||||
Self {
|
||||
kind: SafeBagKind::Unknown,
|
||||
attributes,
|
||||
inner,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_asn1(
|
||||
safe_bag: SafeBagAsn1,
|
||||
crypto_context: &Pkcs12CryptoContext,
|
||||
parsing_params: &Pkcs12ParsingParams,
|
||||
) -> Result<Self, Pkcs12Error> {
|
||||
let attributes = safe_bag
|
||||
.attributes
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(Pkcs12Attribute::from_asn1)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let to_unparsed = |inner, attributes| Self {
|
||||
kind: SafeBagKind::Unknown,
|
||||
attributes,
|
||||
inner,
|
||||
};
|
||||
|
||||
let kind = match &safe_bag.kind {
|
||||
SafeBagKindAsn1::Key(Asn1RawDer(der_data)) => {
|
||||
let key = match PrivateKey::from_pkcs8(&der_data) {
|
||||
Ok(key) => key,
|
||||
Err(_) if parsing_params.skip_soft_parsing_errors => {
|
||||
return Ok(to_unparsed(safe_bag, attributes));
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
SafeBagKind::PrivateKey(key)
|
||||
}
|
||||
SafeBagKindAsn1::EncryptedKey(encrypted_key) => {
|
||||
let encryption = match Pkcs12Encryption::from_asn1(encrypted_key.algorithm.clone()) {
|
||||
Ok(encryption) => encryption,
|
||||
Err(_) if parsing_params.skip_decryption_errors => {
|
||||
return Ok(to_unparsed(safe_bag, attributes));
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
let der_data = match encryption.decrypt(&encrypted_key.encrypted_data.0, crypto_context) {
|
||||
Ok(der_data) => der_data,
|
||||
Err(_) if parsing_params.skip_decryption_errors => {
|
||||
return Ok(to_unparsed(safe_bag, attributes));
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
let key = match PrivateKey::from_pkcs8(&der_data) {
|
||||
Ok(key) => key,
|
||||
Err(_) if parsing_params.skip_soft_parsing_errors => {
|
||||
return Ok(to_unparsed(safe_bag, attributes));
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
SafeBagKind::EncryptedPrivateKey { encryption, key }
|
||||
}
|
||||
SafeBagKindAsn1::Certificate(CertificateBagAsn1::X509(OctetStringAsn1(der_data))) => {
|
||||
let cert = match Cert::from_der(&der_data) {
|
||||
Ok(cert) => cert,
|
||||
Err(_) if parsing_params.skip_soft_parsing_errors => {
|
||||
return Ok(to_unparsed(safe_bag, attributes));
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
SafeBagKind::Certificate(cert)
|
||||
}
|
||||
SafeBagKindAsn1::Secret(SecretBagAsn1 { type_id, value }) => {
|
||||
let secret = SecretSafeBag {
|
||||
oid: type_id.clone(),
|
||||
data: value.clone(),
|
||||
};
|
||||
|
||||
SafeBagKind::Secret(secret)
|
||||
}
|
||||
SafeBagKindAsn1::SafeContents(safe_contents) => {
|
||||
let safe_bags = safe_contents
|
||||
.0
|
||||
.iter()
|
||||
.map(|sb| Self::from_asn1(sb.clone(), crypto_context, parsing_params))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
SafeBagKind::Nested(safe_bags)
|
||||
}
|
||||
SafeBagKindAsn1::Crl(_)
|
||||
| SafeBagKindAsn1::Certificate(CertificateBagAsn1::Unknown { .. })
|
||||
| SafeBagKindAsn1::Unknown { .. } => {
|
||||
return Ok(to_unparsed(safe_bag, attributes));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
kind,
|
||||
attributes,
|
||||
inner: safe_bag,
|
||||
})
|
||||
}
|
||||
|
||||
/// Adds a PKCS12 attribute to this safe bag.
|
||||
///
|
||||
/// Note that there is an additional performance cost: the inner DER representation must be updated.
|
||||
pub fn add_attribute(&mut self, attribute: Pkcs12Attribute) {
|
||||
self.attributes.push(attribute);
|
||||
self.inner.attributes = attributes_to_asn1(&self.attributes);
|
||||
}
|
||||
|
||||
pub fn attributes(&self) -> &[Pkcs12Attribute] {
|
||||
&self.attributes
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &SafeBagKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn into_kind(self) -> SafeBagKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &SafeBagAsn1 {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> SafeBagAsn1 {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed safe bag representation.
|
||||
#[derive(Debug)]
|
||||
pub enum SafeBagKind {
|
||||
PrivateKey(PrivateKey),
|
||||
EncryptedPrivateKey {
|
||||
encryption: Pkcs12Encryption,
|
||||
key: PrivateKey,
|
||||
},
|
||||
Certificate(Cert),
|
||||
Secret(SecretSafeBag),
|
||||
Nested(Vec<SafeBag>),
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Clone for SafeBagKind {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::PrivateKey(key) => Self::PrivateKey(key.clone()),
|
||||
Self::EncryptedPrivateKey { encryption, key } => Self::EncryptedPrivateKey {
|
||||
encryption: encryption.duplicate(),
|
||||
key: key.clone(),
|
||||
},
|
||||
Self::Certificate(cert) => Self::Certificate(cert.clone()),
|
||||
Self::Secret(secret) => Self::Secret(secret.clone()),
|
||||
Self::Nested(nested) => Self::Nested(nested.clone()),
|
||||
Self::Unknown => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Secret bag which could contain any user-defined data, as long as it could be DER-encoded.
|
||||
/// It is advised to use types from `picky-asn1-der` crate.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SecretSafeBag {
|
||||
oid: ObjectIdentifier,
|
||||
data: Asn1RawDer,
|
||||
}
|
||||
|
||||
impl SecretSafeBag {
|
||||
pub fn new_raw(oid: ObjectIdentifier, data: Asn1RawDer) -> Self {
|
||||
Self { oid, data }
|
||||
}
|
||||
|
||||
/// Create new secret bag from serializable data.
|
||||
pub fn new<T: Serialize>(oid: ObjectIdentifier, value: &T) -> Result<Self, Pkcs12Error> {
|
||||
let encoded = picky_asn1_der::to_vec(value)?;
|
||||
Ok(Self {
|
||||
oid,
|
||||
data: Asn1RawDer(encoded),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn oid(&self) -> &ObjectIdentifier {
|
||||
&self.oid
|
||||
}
|
||||
|
||||
pub fn raw_data(&self) -> &[u8] {
|
||||
&self.data.0
|
||||
}
|
||||
|
||||
/// Get secret bag data as deserialized type.
|
||||
pub fn get_data<'a, T: Deserialize<'a>>(&'a self) -> Result<T, Pkcs12Error> {
|
||||
let deserialized = picky_asn1_der::from_bytes(&self.data.0)?;
|
||||
Ok(deserialized)
|
||||
}
|
||||
}
|
||||
|
||||
fn attributes_to_asn1(attributes: &[Pkcs12Attribute]) -> Option<Vec<Pkcs12AttributeAsn1>> {
|
||||
if attributes.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(attributes.iter().map(|a| a.inner().clone()).collect())
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
use crate::pkcs12::{Pkcs12CryptoContext, Pkcs12Encryption, Pkcs12Error, Pkcs12ParsingParams, SafeBag};
|
||||
use picky_asn1::wrapper::OctetStringAsn1;
|
||||
use picky_asn1_x509::pkcs12::{
|
||||
EncryptedSafeContents as EncryptedSafeContentsAsn1, SafeContents as SafeContentsAsn1,
|
||||
SafeContentsContentInfo as SafeContentsContentInfoAsn1,
|
||||
};
|
||||
|
||||
/// Top-level PFX container object, which holds list of safe bags and could be encrypted or not.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SafeContents {
|
||||
kind: SafeContentsKind,
|
||||
inner: SafeContentsContentInfoAsn1,
|
||||
}
|
||||
|
||||
impl SafeContents {
|
||||
pub(crate) fn from_asn1(
|
||||
inner: SafeContentsContentInfoAsn1,
|
||||
crypto_context: &Pkcs12CryptoContext,
|
||||
parsing_params: &Pkcs12ParsingParams,
|
||||
) -> Result<Self, Pkcs12Error> {
|
||||
let to_unparsed = |inner| Self {
|
||||
kind: SafeContentsKind::Unknown,
|
||||
inner,
|
||||
};
|
||||
|
||||
let kind = match &inner {
|
||||
SafeContentsContentInfoAsn1::Data(data) => {
|
||||
let safe_bags = data
|
||||
.0
|
||||
.iter()
|
||||
.map(|sb| SafeBag::from_asn1(sb.clone(), crypto_context, parsing_params))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Self {
|
||||
kind: SafeContentsKind::SafeBags(safe_bags),
|
||||
inner,
|
||||
}
|
||||
}
|
||||
SafeContentsContentInfoAsn1::EncryptedData(encrypted) => {
|
||||
let encryption = match Pkcs12Encryption::from_asn1(encrypted.algorithm.clone()) {
|
||||
Ok(encryption) => encryption,
|
||||
Err(_) if parsing_params.skip_decryption_errors => {
|
||||
return Ok(to_unparsed(inner));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let encrypted_content = match encrypted.encrypted_content.as_ref() {
|
||||
Some(content) => content.0.as_slice(),
|
||||
None => {
|
||||
return Ok(Self {
|
||||
// No content to decrypt
|
||||
kind: SafeContentsKind::EncryptedSafeBags {
|
||||
encryption,
|
||||
safe_bags: vec![],
|
||||
},
|
||||
inner,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let decrypted = match encryption.decrypt(encrypted_content, crypto_context) {
|
||||
Ok(decrypted) => decrypted,
|
||||
Err(_) if parsing_params.skip_decryption_errors => {
|
||||
return Ok(to_unparsed(inner));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let safe_bags_asn1 = match picky_asn1_der::from_bytes::<SafeContentsAsn1>(&decrypted) {
|
||||
Ok(safe_contents) => safe_contents.0,
|
||||
Err(_) if parsing_params.skip_decryption_errors => {
|
||||
return Ok(to_unparsed(inner));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
let safe_bags = safe_bags_asn1
|
||||
.into_iter()
|
||||
.map(|sb| SafeBag::from_asn1(sb, crypto_context, parsing_params))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Self {
|
||||
kind: SafeContentsKind::EncryptedSafeBags { encryption, safe_bags },
|
||||
inner,
|
||||
}
|
||||
}
|
||||
SafeContentsContentInfoAsn1::Unknown { .. } => return Ok(to_unparsed(inner)),
|
||||
};
|
||||
|
||||
Ok(kind)
|
||||
}
|
||||
|
||||
pub fn new(safe_bags: Vec<SafeBag>) -> Self {
|
||||
let safe_contents = SafeContentsAsn1(safe_bags.iter().map(|sb| sb.inner().clone()).collect());
|
||||
Self {
|
||||
kind: SafeContentsKind::SafeBags(safe_bags),
|
||||
inner: SafeContentsContentInfoAsn1::Data(safe_contents),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_encrypted(
|
||||
safe_bags: Vec<SafeBag>,
|
||||
encryption: Pkcs12Encryption,
|
||||
crypto_context: &Pkcs12CryptoContext,
|
||||
) -> Result<Self, Pkcs12Error> {
|
||||
let safe_contents = SafeContentsAsn1(safe_bags.iter().map(|sb| sb.inner().clone()).collect());
|
||||
let der_data = picky_asn1_der::to_vec(&safe_contents)?;
|
||||
let encrypted = encryption.encrypt(&der_data, crypto_context)?;
|
||||
|
||||
let inner = SafeContentsContentInfoAsn1::EncryptedData(EncryptedSafeContentsAsn1 {
|
||||
algorithm: encryption.inner().clone(),
|
||||
encrypted_content: Some(OctetStringAsn1(encrypted)),
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
kind: SafeContentsKind::EncryptedSafeBags { encryption, safe_bags },
|
||||
inner,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &SafeContentsKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn into_kind(self) -> SafeContentsKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &SafeContentsContentInfoAsn1 {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> SafeContentsContentInfoAsn1 {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
// Clippy triggers lint because of relatively big `Pkcs12Encryption` (~200 bytes) in comparison with
|
||||
// `SafeContentsKind::Unknown` (0 bytes), but just to keep it consistent with other enums, we do allow
|
||||
// such difference in size.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum SafeContentsKind {
|
||||
SafeBags(Vec<SafeBag>),
|
||||
EncryptedSafeBags {
|
||||
encryption: Pkcs12Encryption,
|
||||
safe_bags: Vec<SafeBag>,
|
||||
},
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Clone for SafeContentsKind {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::SafeBags(bags) => Self::SafeBags(bags.clone()),
|
||||
Self::EncryptedSafeBags { encryption, safe_bags } => Self::EncryptedSafeBags {
|
||||
encryption: encryption.duplicate(),
|
||||
safe_bags: safe_bags.clone(),
|
||||
},
|
||||
Self::Unknown => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum PuttyError {
|
||||
#[error("end of input")]
|
||||
EndOfInput,
|
||||
#[error("invalid input")]
|
||||
InvalidInput {
|
||||
context: &'static str,
|
||||
expected: &'static str,
|
||||
actual: String,
|
||||
},
|
||||
#[error("invalid key value format")]
|
||||
InvalidKeyValueFormat,
|
||||
#[error("AES encryption failed")]
|
||||
Aes,
|
||||
#[error("invalid argon2 params")]
|
||||
Argon2,
|
||||
#[error("MAC validation failed (wrong password or corrupted data)")]
|
||||
MacValidation,
|
||||
#[error("public and private key mismatch")]
|
||||
PublicAndPrivateKeyMismatch,
|
||||
#[error("invalid private key data")]
|
||||
InvalidPrivateKeyData,
|
||||
#[error("invalid public key data")]
|
||||
InvalidPublicKeyData,
|
||||
#[error("invalid public key container")]
|
||||
InvalidPublicKeyContainer,
|
||||
#[error("invalid public key comment")]
|
||||
InvalidPublicKeyComment,
|
||||
#[error("private key is already decrypted")]
|
||||
AlreadyDecrypted,
|
||||
#[error("private key is already encrypted")]
|
||||
AlreadyEncrypted,
|
||||
#[error("private key decryption is required prior to this operation")]
|
||||
Encrypted,
|
||||
#[error("unsupported feature")]
|
||||
NotSupported { feature: &'static str },
|
||||
#[error("RSA params precomputation failed")]
|
||||
RsaPrecompute,
|
||||
#[error("RSA primes count should be exactly 2")]
|
||||
RsaInvalidPrimesCount { count: usize },
|
||||
#[error(transparent)]
|
||||
SshPublicKey(#[from] crate::ssh::public_key::SshPublicKeyError),
|
||||
#[error(transparent)]
|
||||
SshPivateKey(#[from] crate::ssh::private_key::SshPrivateKeyError),
|
||||
#[error(transparent)]
|
||||
KeyError(#[from] crate::key::KeyError),
|
||||
#[error(transparent)]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
OutIsTooSmallError(#[from] inout::OutIsTooSmallError),
|
||||
#[error(transparent)]
|
||||
RandError(#[from] rand::rngs::SysError),
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
macro_rules! ppk_const {
|
||||
($name:ident, $key:expr) => {
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct $name;
|
||||
|
||||
impl std::str::FromStr for $name {
|
||||
type Err = $crate::putty::key_value::PpkValueParsingError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s == $key {
|
||||
Ok(Self)
|
||||
} else {
|
||||
Err($crate::putty::key_value::PpkValueParsingError {
|
||||
expected: $key,
|
||||
actual: s.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for $name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str($key)
|
||||
}
|
||||
}
|
||||
|
||||
impl PpkLiteral for $name {
|
||||
fn context() -> &'static str {
|
||||
stringify!($name)
|
||||
}
|
||||
|
||||
fn as_static_str(&self) -> &'static str {
|
||||
$key
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_ppk_enum_expected_str {
|
||||
($first:expr, $($keys:expr),+) => {
|
||||
concat!($first, ", ", $($keys),+)
|
||||
};
|
||||
($first:expr) => {
|
||||
$first
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! ppk_enum {
|
||||
($name:ident, $($variant:ident => $key:expr),+) => {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum $name {
|
||||
$($variant),+
|
||||
}
|
||||
|
||||
impl std::str::FromStr for $name {
|
||||
type Err = $crate::putty::key_value::PpkValueParsingError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
$($key => Ok(Self::$variant),)+
|
||||
_ => Err($crate::putty::key_value::PpkValueParsingError {
|
||||
expected: concat!("[", impl_ppk_enum_expected_str!($($key),+), "]"),
|
||||
actual: s.to_string()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for $name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
$(Self::$variant => f.write_str($key),)+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PpkLiteral for $name {
|
||||
fn context() -> &'static str {
|
||||
stringify!($name)
|
||||
}
|
||||
|
||||
fn as_static_str(&self) -> &'static str {
|
||||
match self {
|
||||
$(Self::$variant => $key,)+
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! ppk_generic_value {
|
||||
($name:ident, $type:ident) => {
|
||||
pub struct $name($type);
|
||||
|
||||
impl std::str::FromStr for $name {
|
||||
type Err = $crate::putty::key_value::PpkValueParsingError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
s.parse()
|
||||
.map(Self)
|
||||
.map_err(|_| $crate::putty::key_value::PpkValueParsingError {
|
||||
expected: concat!("<valid ", stringify!($type), " value>"),
|
||||
actual: s.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for $name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$type> for $name {
|
||||
fn from(value: $type) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$name> for $type {
|
||||
fn from(value: $name) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! ppk_key_value {
|
||||
($name:ident, $key:ident, $value:ident) => {
|
||||
pub struct $name;
|
||||
|
||||
impl $crate::putty::key_value::PpkKeyValue for $name {
|
||||
type Key = $key;
|
||||
type Value = $value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! ppk_multiline_key_value {
|
||||
($name:ident, $key:ident, $value:ident) => {
|
||||
pub struct $name;
|
||||
|
||||
impl $crate::putty::key_value::PpkMultilineKeyValue for $name {
|
||||
type Key = $key;
|
||||
type Value = $value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use {
|
||||
impl_ppk_enum_expected_str, ppk_const, ppk_enum, ppk_generic_value, ppk_key_value, ppk_multiline_key_value,
|
||||
};
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
//! This module provides a set of traits and macros and types to parse and write PuTTY key-value
|
||||
//! format in strongly-typed manner.
|
||||
|
||||
mod macros;
|
||||
mod reader;
|
||||
mod writer;
|
||||
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_ENGINE;
|
||||
|
||||
use self::macros::*;
|
||||
|
||||
pub(crate) use reader::PuttyKvReader;
|
||||
pub(crate) use writer::PuttyKvWriter;
|
||||
|
||||
pub struct PpkValueParsingError {
|
||||
pub expected: &'static str,
|
||||
pub actual: String,
|
||||
}
|
||||
|
||||
const KV_DELIMITER: &str = ": ";
|
||||
|
||||
/// Trait for keys/values that could be represented as singular or set of static strings.
|
||||
pub(crate) trait PpkLiteral {
|
||||
fn context() -> &'static str;
|
||||
fn as_static_str(&self) -> &'static str;
|
||||
}
|
||||
|
||||
/// Trait for key-value pairs that use multiline format.
|
||||
pub(crate) trait PpkMultilineKeyValue {
|
||||
type Key: FromStr<Err = PpkValueParsingError> + ToString + PpkLiteral;
|
||||
type Value: FromStr<Err = PpkValueParsingError> + ToString;
|
||||
}
|
||||
|
||||
/// Trait for key-value pairs that use single-line format.
|
||||
pub(crate) trait PpkKeyValue {
|
||||
type Key: FromStr<Err = PpkValueParsingError> + ToString + PpkLiteral;
|
||||
type Value: FromStr<Err = PpkValueParsingError> + ToString;
|
||||
}
|
||||
|
||||
/// Wrapper type for base64 multiline data inside PPK file.
|
||||
pub(crate) struct Base64PpkValue(Vec<u8>);
|
||||
|
||||
impl FromStr for Base64PpkValue {
|
||||
type Err = PpkValueParsingError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
BASE64_ENGINE
|
||||
.decode(s)
|
||||
.map_err(|_| PpkValueParsingError {
|
||||
expected: "<valid base64 data>",
|
||||
actual: s.to_string(),
|
||||
})
|
||||
.map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Base64PpkValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", BASE64_ENGINE.encode(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Base64PpkValue {
|
||||
fn from(value: Vec<u8>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Base64PpkValue> for Vec<u8> {
|
||||
fn from(value: Base64PpkValue) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper type for hex-string multiline data inside PPK file.
|
||||
pub(crate) struct HexPpkValue(Vec<u8>);
|
||||
|
||||
impl FromStr for HexPpkValue {
|
||||
type Err = PpkValueParsingError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
hex::decode(s)
|
||||
.map_err(|_| PpkValueParsingError {
|
||||
expected: "<valid hex data>",
|
||||
actual: s.to_string(),
|
||||
})
|
||||
.map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HexPpkValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", hex::encode(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for HexPpkValue {
|
||||
fn from(value: Vec<u8>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HexPpkValue> for Vec<u8> {
|
||||
fn from(value: HexPpkValue) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
// Key value type definitions
|
||||
|
||||
ppk_enum!(
|
||||
PpkVersionKey,
|
||||
V2 => "PuTTY-User-Key-File-2",
|
||||
V3 => "PuTTY-User-Key-File-3"
|
||||
);
|
||||
ppk_enum!(
|
||||
PpkKeyAlgorithmValue,
|
||||
Rsa => "ssh-rsa",
|
||||
Dss => "ssh-dss",
|
||||
EcdsaSha2Nistp256 => "ecdsa-sha2-nistp256",
|
||||
EcdsaSha2Nistp384 => "ecdsa-sha2-nistp384",
|
||||
EcdsaSha2Nistp521 => "ecdsa-sha2-nistp521",
|
||||
Ed25519 => "ssh-ed25519",
|
||||
Ed448 => "ssh-ed448"
|
||||
);
|
||||
ppk_key_value!(PpkHeader, PpkVersionKey, PpkKeyAlgorithmValue);
|
||||
|
||||
ppk_const!(PpkEncryptionKey, "Encryption");
|
||||
ppk_enum!(
|
||||
PpkEncryptionValue,
|
||||
None => "none",
|
||||
Aes256Cbc => "aes256-cbc"
|
||||
);
|
||||
ppk_key_value!(PpkEncryption, PpkEncryptionKey, PpkEncryptionValue);
|
||||
|
||||
ppk_const!(PpkCommentKey, "Comment");
|
||||
ppk_generic_value!(PpkCommentValue, String);
|
||||
ppk_key_value!(PpkComment, PpkCommentKey, PpkCommentValue);
|
||||
|
||||
ppk_const!(PpkPublicLinesKey, "Public-Lines");
|
||||
ppk_multiline_key_value!(PpkPublicLines, PpkPublicLinesKey, Base64PpkValue);
|
||||
|
||||
ppk_const!(PpkKeyDerivationKey, "Key-Derivation");
|
||||
ppk_enum!(
|
||||
Argon2FlavourValue,
|
||||
Argon2d => "Argon2d",
|
||||
Argon2i => "Argon2i",
|
||||
Argon2id => "Argon2id"
|
||||
);
|
||||
ppk_key_value!(PpkKeyDerivation, PpkKeyDerivationKey, Argon2FlavourValue);
|
||||
ppk_const!(PpkArgon2MemoryKey, "Argon2-Memory");
|
||||
ppk_generic_value!(PpkArgon2MemoryValue, u32);
|
||||
ppk_key_value!(PpkArgon2Memory, PpkArgon2MemoryKey, PpkArgon2MemoryValue);
|
||||
ppk_const!(PpkArgon2PassesKey, "Argon2-Passes");
|
||||
ppk_generic_value!(PpkArgon2PassesValue, u32);
|
||||
ppk_key_value!(PpkArgon2Passes, PpkArgon2PassesKey, PpkArgon2PassesValue);
|
||||
ppk_const!(PpkArgon2ParallelismKey, "Argon2-Parallelism");
|
||||
ppk_generic_value!(PpkArgon2ParallelismValue, u32);
|
||||
ppk_key_value!(PpkArgon2Parallelism, PpkArgon2ParallelismKey, PpkArgon2ParallelismValue);
|
||||
ppk_const!(PpkArgon2SaltKey, "Argon2-Salt");
|
||||
ppk_key_value!(PpkArgon2Salt, PpkArgon2SaltKey, HexPpkValue);
|
||||
|
||||
ppk_const!(PpkPrivateLinesKey, "Private-Lines");
|
||||
ppk_multiline_key_value!(PpkPrivateLines, PpkPrivateLinesKey, Base64PpkValue);
|
||||
|
||||
ppk_const!(PpkPrivateMacKey, "Private-MAC");
|
||||
ppk_key_value!(PpkPrivateMac, PpkPrivateMacKey, HexPpkValue);
|
||||
|
||||
impl From<Argon2FlavourValue> for argon2::Algorithm {
|
||||
fn from(value: Argon2FlavourValue) -> Self {
|
||||
match value {
|
||||
Argon2FlavourValue::Argon2d => argon2::Algorithm::Argon2d,
|
||||
Argon2FlavourValue::Argon2i => argon2::Algorithm::Argon2i,
|
||||
Argon2FlavourValue::Argon2id => argon2::Algorithm::Argon2id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PpkKeyAlgorithmValue {
|
||||
pub fn key_mpint_values_count(&self) -> usize {
|
||||
match self {
|
||||
PpkKeyAlgorithmValue::Rsa => 4,
|
||||
PpkKeyAlgorithmValue::Dss
|
||||
| PpkKeyAlgorithmValue::EcdsaSha2Nistp256
|
||||
| PpkKeyAlgorithmValue::EcdsaSha2Nistp384
|
||||
| PpkKeyAlgorithmValue::EcdsaSha2Nistp521
|
||||
| PpkKeyAlgorithmValue::Ed25519
|
||||
| PpkKeyAlgorithmValue::Ed448 => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
use super::{KV_DELIMITER, PpkKeyValue, PpkLiteral, PpkMultilineKeyValue, PpkValueParsingError};
|
||||
use crate::putty::PuttyError;
|
||||
|
||||
/// Reader for PPK key-value format.
|
||||
pub struct PuttyKvReader<'a> {
|
||||
input: std::str::Lines<'a>,
|
||||
}
|
||||
|
||||
impl<'a> PuttyKvReader<'a> {
|
||||
pub fn from_str(input: &'a str) -> Self {
|
||||
Self { input: input.lines() }
|
||||
}
|
||||
|
||||
pub fn next_value<T: PpkKeyValue>(&mut self) -> Result<T::Value, PuttyError> {
|
||||
let (_, value) = self.next_key_value::<T>()?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn next_key_value<T: PpkKeyValue>(&mut self) -> Result<(T::Key, T::Value), PuttyError> {
|
||||
let line = self.input.next().ok_or(PuttyError::EndOfInput)?;
|
||||
let (key, value) = line.split_once(KV_DELIMITER).ok_or(PuttyError::InvalidKeyValueFormat)?;
|
||||
|
||||
let parsed_key = key
|
||||
.parse()
|
||||
.map_err(|e: PpkValueParsingError| PuttyError::InvalidInput {
|
||||
context: T::Key::context(),
|
||||
expected: e.expected,
|
||||
actual: e.actual,
|
||||
})?;
|
||||
|
||||
let parsed_value = value
|
||||
.parse()
|
||||
.map_err(|e: PpkValueParsingError| PuttyError::InvalidInput {
|
||||
context: T::Key::context(),
|
||||
expected: e.expected,
|
||||
actual: e.actual,
|
||||
})?;
|
||||
|
||||
Ok((parsed_key, parsed_value))
|
||||
}
|
||||
|
||||
pub fn next_multiline_value<T: PpkMultilineKeyValue>(&mut self) -> Result<T::Value, PuttyError> {
|
||||
let (_, value) = self.next_multiline_key_value::<T>()?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn next_multiline_key_value<T: PpkMultilineKeyValue>(&mut self) -> Result<(T::Key, T::Value), PuttyError> {
|
||||
let line = self.input.next().ok_or(PuttyError::EndOfInput)?;
|
||||
let (key, value) = line.split_once(KV_DELIMITER).ok_or(PuttyError::InvalidKeyValueFormat)?;
|
||||
|
||||
// Parse key early to check if it's valid before reading multiline value
|
||||
let parsed_key: T::Key = key
|
||||
.parse()
|
||||
.map_err(|e: PpkValueParsingError| PuttyError::InvalidInput {
|
||||
context: T::Key::context(),
|
||||
expected: e.expected,
|
||||
actual: e.actual,
|
||||
})?;
|
||||
|
||||
// NOTE: u16 is enough for multiline fields, as in PPK format they are storing
|
||||
// base64-encoded private/public key data, and 65535 lines is more than enough for any
|
||||
// supported PPK key type.
|
||||
let lines_count: u16 = value.parse().map_err(|_| PuttyError::InvalidInput {
|
||||
context: T::Key::context(),
|
||||
expected: "<valid lines count>",
|
||||
actual: value.to_string(),
|
||||
})?;
|
||||
|
||||
let mut encoded = String::new();
|
||||
|
||||
for _ in 0..lines_count {
|
||||
// NOTE: we do not preserve newlines for multiline fields as in PPK they are
|
||||
// only used to split base64-encoded data into lines.
|
||||
encoded.push_str(self.input.next().ok_or(PuttyError::EndOfInput)?);
|
||||
}
|
||||
|
||||
let parsed_value = encoded
|
||||
.parse()
|
||||
.map_err(|e: PpkValueParsingError| PuttyError::InvalidInput {
|
||||
context: T::Key::context(),
|
||||
expected: e.expected,
|
||||
actual: e.actual,
|
||||
})?;
|
||||
|
||||
Ok((parsed_key, parsed_value))
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
use super::{KV_DELIMITER, PpkKeyValue, PpkLiteral as _, PpkMultilineKeyValue};
|
||||
|
||||
/// Writer for PPK key-value format.
|
||||
pub(crate) struct PuttyKvWriter {
|
||||
output: String,
|
||||
line_end: &'static str,
|
||||
}
|
||||
|
||||
impl PuttyKvWriter {
|
||||
pub fn new(crlf: bool) -> Self {
|
||||
Self {
|
||||
output: String::new(),
|
||||
line_end: if crlf { "\r\n" } else { "\n" },
|
||||
}
|
||||
}
|
||||
|
||||
fn write_key_value_impl(&mut self, key: &str, value: &str) {
|
||||
self.output.push_str(key);
|
||||
self.output.push_str(KV_DELIMITER);
|
||||
self.output.push_str(value);
|
||||
self.output.push_str(self.line_end);
|
||||
}
|
||||
|
||||
pub fn write_value<T: PpkKeyValue>(&mut self, value: T::Value)
|
||||
where
|
||||
T::Key: Default,
|
||||
{
|
||||
self.write_key_value::<T>(T::Key::default(), value);
|
||||
}
|
||||
|
||||
pub fn write_key_value<T: PpkKeyValue>(&mut self, key: T::Key, value: T::Value) {
|
||||
self.write_key_value_impl(key.as_static_str(), &value.to_string());
|
||||
}
|
||||
|
||||
pub fn write_multiline_value<T: PpkMultilineKeyValue>(&mut self, value: T::Value)
|
||||
where
|
||||
T::Key: Default,
|
||||
{
|
||||
self.write_multiline_key_value::<T>(T::Key::default(), value);
|
||||
}
|
||||
|
||||
pub fn write_multiline_key_value<T: PpkMultilineKeyValue>(&mut self, key: T::Key, value: T::Value) {
|
||||
// PuTTY uses a maximum of 64 characters per line
|
||||
const MAX_CHARS_PER_LINE: usize = 64;
|
||||
|
||||
let value = value.to_string();
|
||||
|
||||
let lines_count = value.len() / MAX_CHARS_PER_LINE + (value.len() % MAX_CHARS_PER_LINE != 0) as usize;
|
||||
|
||||
self.write_key_value_impl(key.as_static_str(), &lines_count.to_string());
|
||||
|
||||
let mut value_remaining = value.as_str();
|
||||
|
||||
while !value_remaining.is_empty() {
|
||||
let line_len = value_remaining.len().min(MAX_CHARS_PER_LINE);
|
||||
let (line, remaining) = value_remaining.split_at(line_len);
|
||||
value_remaining = remaining;
|
||||
|
||||
self.output.push_str(line);
|
||||
self.output.push_str(self.line_end);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(self) -> String {
|
||||
self.output
|
||||
}
|
||||
}
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
//! PuTTY key format described in [Appendix C][1] of the PuTTY User Manual.
|
||||
//!
|
||||
//! Both private([`Ppk`]) and public([`PuttyPublicKey`]) keys are supported.
|
||||
//!
|
||||
//! [1]: https://the.earth.li/~sgtatham/putty/0.75/htmldoc/AppendixC.html#ppk
|
||||
|
||||
mod error;
|
||||
mod key_value;
|
||||
mod ppk;
|
||||
mod private_key;
|
||||
mod public_key;
|
||||
|
||||
pub use error::PuttyError;
|
||||
pub use key_value::{
|
||||
Argon2FlavourValue as Argon2Flavour, PpkKeyAlgorithmValue as PpkKeyAlgorithm, PpkVersionKey as PpkVersion,
|
||||
};
|
||||
pub use ppk::{Argon2Params, Ppk, PpkEncryptionConfig, PpkEncryptionConfigBuilder};
|
||||
pub use public_key::PuttyPublicKey;
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
//! AES encryption and decryption utilities.
|
||||
|
||||
use crate::putty::PuttyError;
|
||||
use aes::cipher::KeyIvInit;
|
||||
use aes::cipher::block_padding::NoPadding;
|
||||
use cbc::cipher::{BlockModeDecrypt, BlockModeEncrypt};
|
||||
use inout::InOutBufReserved;
|
||||
use rand_core::Rng;
|
||||
|
||||
pub const KEY_SIZE: usize = 32;
|
||||
pub const BLOCK_SIZE: usize = 16;
|
||||
|
||||
/// Adds padding to the message if it is not a multiple of the AES block size.
|
||||
pub fn make_padding<R: Rng>(mut message: Vec<u8>, mut rng: R) -> Vec<u8> {
|
||||
if message.len() % BLOCK_SIZE != 0 {
|
||||
let unpadded_size = message.len();
|
||||
let padding_size = BLOCK_SIZE - (unpadded_size % BLOCK_SIZE);
|
||||
|
||||
message.resize(unpadded_size + padding_size, 0);
|
||||
rng.fill_bytes(&mut message[unpadded_size..]);
|
||||
}
|
||||
|
||||
message
|
||||
}
|
||||
|
||||
/// Encrypts the message in-place using AES-256 in CBC mode.
|
||||
pub fn encrypt(message: &mut [u8], key: &[u8], iv: &[u8]) -> Result<(), PuttyError> {
|
||||
let encryptor = cbc::Encryptor::<aes::Aes256>::new_from_slices(key, iv).map_err(|_| PuttyError::Aes)?;
|
||||
|
||||
let inout = InOutBufReserved::from_mut_slice(message, message.len())?;
|
||||
encryptor
|
||||
.encrypt_padded_inout::<NoPadding>(inout)
|
||||
.map_err(|_| PuttyError::Aes)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decrypts the message in-place using AES-256 in CBC mode.
|
||||
pub fn decrypt(message: &mut [u8], key: &[u8], iv: &[u8]) -> Result<(), PuttyError> {
|
||||
let decryptor = cbc::Decryptor::<aes::Aes256>::new_from_slices(key, iv).map_err(|_| PuttyError::Aes)?;
|
||||
|
||||
let _ = decryptor
|
||||
.decrypt_padded_inout::<NoPadding>(message.into())
|
||||
.map_err(|_| PuttyError::Aes);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
//! PPK encoding and decoding functions.
|
||||
|
||||
use crate::putty::key_value::{
|
||||
Base64PpkValue, HexPpkValue, PpkArgon2Memory, PpkArgon2MemoryValue, PpkArgon2Parallelism,
|
||||
PpkArgon2ParallelismValue, PpkArgon2Passes, PpkArgon2PassesValue, PpkArgon2Salt, PpkComment, PpkCommentValue,
|
||||
PpkEncryption, PpkEncryptionValue, PpkHeader, PpkKeyDerivation, PpkPrivateLines, PpkPrivateMac, PpkPublicLines,
|
||||
PpkVersionKey, PuttyKvReader, PuttyKvWriter,
|
||||
};
|
||||
use crate::putty::ppk::encryption::PpkEncryptionKind;
|
||||
use crate::putty::{Argon2Params, Ppk, PuttyError};
|
||||
use std::str::FromStr;
|
||||
|
||||
impl FromStr for Ppk {
|
||||
type Err = PuttyError;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let mut reader = PuttyKvReader::from_str(input);
|
||||
|
||||
let (version, algorithm) = reader.next_key_value::<PpkHeader>()?;
|
||||
let encryption = reader.next_value::<PpkEncryption>()?;
|
||||
let comment = reader.next_value::<PpkComment>()?;
|
||||
let public_key = reader.next_multiline_value::<PpkPublicLines>()?;
|
||||
|
||||
let encryption = match encryption {
|
||||
PpkEncryptionValue::None => None,
|
||||
PpkEncryptionValue::Aes256Cbc if version == PpkVersionKey::V2 => Some(PpkEncryptionKind::Aes256CbcV2),
|
||||
PpkEncryptionValue::Aes256Cbc => {
|
||||
let argon2_flavor = reader.next_value::<PpkKeyDerivation>()?;
|
||||
let argon2_memory = reader.next_value::<PpkArgon2Memory>()?;
|
||||
let argon2_passes = reader.next_value::<PpkArgon2Passes>()?;
|
||||
let argon2_parallelism = reader.next_value::<PpkArgon2Parallelism>()?;
|
||||
let argon2_salt = reader.next_value::<PpkArgon2Salt>()?;
|
||||
|
||||
Some(PpkEncryptionKind::Aes256CbcV3(Argon2Params {
|
||||
flavor: argon2_flavor,
|
||||
memory: argon2_memory.into(),
|
||||
passes: argon2_passes.into(),
|
||||
parallelism: argon2_parallelism.into(),
|
||||
salt: argon2_salt.into(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
let private_key = reader.next_multiline_value::<PpkPrivateLines>()?;
|
||||
let mac = reader.next_value::<PpkPrivateMac>()?;
|
||||
|
||||
let ppk = Ppk {
|
||||
version,
|
||||
algorithm,
|
||||
encryption,
|
||||
comment: comment.into(),
|
||||
public_key: public_key.into(),
|
||||
private_key: private_key.into(),
|
||||
mac: mac.into(),
|
||||
};
|
||||
|
||||
// Validate MAC for file integrity check
|
||||
if ppk.encryption.is_none() {
|
||||
let mac = ppk.calculate_unencrypted_mac(ppk.private_key.as_slice())?;
|
||||
|
||||
if mac.as_slice() != ppk.mac.as_slice() {
|
||||
return Err(PuttyError::MacValidation);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ppk)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ppk {
|
||||
/// Encodes the PPK key to a string.
|
||||
pub fn to_string(&self) -> Result<String, PuttyError> {
|
||||
// NOTE: V2 uses CRLF line endings, V3 uses LF
|
||||
let crlf = match self.version {
|
||||
PpkVersionKey::V2 => true,
|
||||
PpkVersionKey::V3 => false,
|
||||
};
|
||||
|
||||
let mut writer = PuttyKvWriter::new(crlf);
|
||||
|
||||
writer.write_key_value::<PpkHeader>(self.version, self.algorithm);
|
||||
writer.write_value::<PpkEncryption>((self.encryption.as_ref()).into());
|
||||
writer.write_value::<PpkComment>(PpkCommentValue::from(self.comment.clone()));
|
||||
writer.write_multiline_value::<PpkPublicLines>(Base64PpkValue::from(self.public_key.clone()));
|
||||
match &self.encryption {
|
||||
Some(PpkEncryptionKind::Aes256CbcV3(argon2)) => {
|
||||
writer.write_value::<PpkKeyDerivation>(argon2.flavor);
|
||||
writer.write_value::<PpkArgon2Memory>(PpkArgon2MemoryValue::from(argon2.memory));
|
||||
writer.write_value::<PpkArgon2Passes>(PpkArgon2PassesValue::from(argon2.passes));
|
||||
writer.write_value::<PpkArgon2Parallelism>(PpkArgon2ParallelismValue::from(argon2.parallelism));
|
||||
writer.write_value::<PpkArgon2Salt>(HexPpkValue::from(argon2.salt.clone()));
|
||||
}
|
||||
None | Some(PpkEncryptionKind::Aes256CbcV2) => {}
|
||||
}
|
||||
|
||||
writer.write_multiline_value::<PpkPrivateLines>(Base64PpkValue::from(self.private_key.clone()));
|
||||
writer.write_value::<PpkPrivateMac>(HexPpkValue::from(self.mac.clone()));
|
||||
|
||||
Ok(writer.finish())
|
||||
}
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
//! PPK encryption/decryption types and fucntions
|
||||
|
||||
use crate::putty::PuttyError;
|
||||
use crate::putty::key_value::{Argon2FlavourValue, PpkEncryptionValue, PpkVersionKey};
|
||||
use crate::putty::ppk::kdf::{self, KeyMaterialV2};
|
||||
use crate::putty::ppk::{Argon2Params, Ppk, aes as ppk_aes};
|
||||
use crate::ssh::decode::SshReadExt;
|
||||
use rand::rngs::{StdRng, SysRng};
|
||||
use rand_core::SeedableRng as _;
|
||||
|
||||
/// PPK encryption configuration builder.
|
||||
///
|
||||
/// Could be constructed via [`PpkEncryptionConfig::builder()`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PpkEncryptionConfigBuilder {
|
||||
inner: PpkEncryptionConfig,
|
||||
}
|
||||
|
||||
impl PpkEncryptionConfigBuilder {
|
||||
pub fn argon2_flavour(mut self, argon2_flavour: Argon2FlavourValue) -> Self {
|
||||
self.inner.argon2_flavour = argon2_flavour;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn argon2_memory(mut self, argon2_memory: u32) -> Self {
|
||||
self.inner.argon2_memory = argon2_memory;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn argon2_passes(mut self, argon2_passes: u32) -> Self {
|
||||
self.inner.argon2_passes = argon2_passes;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn argon2_parallelism(mut self, argon2_parallelism: u32) -> Self {
|
||||
self.inner.argon2_parallelism = argon2_parallelism;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn argon2_salt_size(mut self, argon2_salt_size: u32) -> Self {
|
||||
self.inner.argon2_salt_size = argon2_salt_size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> PpkEncryptionConfig {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
/// PPK encryption configuration.
|
||||
///
|
||||
/// Could be either constructed via [`Default::default()`] or [`PpkEncryptionConfig::builder()`]
|
||||
///
|
||||
/// Defaults are the same as in PuTTY.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PpkEncryptionConfig {
|
||||
argon2_flavour: Argon2FlavourValue,
|
||||
argon2_memory: u32,
|
||||
argon2_passes: u32,
|
||||
argon2_parallelism: u32,
|
||||
argon2_salt_size: u32,
|
||||
}
|
||||
|
||||
impl Default for PpkEncryptionConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
argon2_flavour: Argon2FlavourValue::Argon2id,
|
||||
argon2_memory: 8192,
|
||||
argon2_passes: 34,
|
||||
argon2_parallelism: 1,
|
||||
argon2_salt_size: 16,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PpkEncryptionConfig {
|
||||
pub fn builder() -> PpkEncryptionConfigBuilder {
|
||||
PpkEncryptionConfigBuilder {
|
||||
inner: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum PpkEncryptionKind {
|
||||
Aes256CbcV2,
|
||||
Aes256CbcV3(Argon2Params),
|
||||
}
|
||||
|
||||
impl From<Option<&PpkEncryptionKind>> for PpkEncryptionValue {
|
||||
fn from(params: Option<&PpkEncryptionKind>) -> Self {
|
||||
match params {
|
||||
None => PpkEncryptionValue::None,
|
||||
Some(PpkEncryptionKind::Aes256CbcV2) => PpkEncryptionValue::Aes256Cbc,
|
||||
Some(PpkEncryptionKind::Aes256CbcV3(_)) => PpkEncryptionValue::Aes256Cbc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ppk {
|
||||
/// Returns true if the key is encrypted
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
self.encryption.is_some()
|
||||
}
|
||||
|
||||
/// Argon2 KDF parameters if encryption is used (V3 only)
|
||||
pub fn argon2_params(&self) -> Option<&Argon2Params> {
|
||||
match &self.encryption {
|
||||
Some(PpkEncryptionKind::Aes256CbcV3(params)) => Some(params),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns PPK key encrypted with the specified passphrase and config.
|
||||
pub fn encrypt(&self, passphrase: &str, config: PpkEncryptionConfig) -> Result<Self, PuttyError> {
|
||||
self.encrypt_with_rng(passphrase, config, StdRng::try_from_rng(&mut SysRng)?)
|
||||
}
|
||||
|
||||
/// Returns PPK key encrypted with the specified passphrase, config and RNG.
|
||||
pub fn encrypt_with_rng(
|
||||
&self,
|
||||
passphrase: &str,
|
||||
config: PpkEncryptionConfig,
|
||||
mut rng: impl rand_core::Rng,
|
||||
) -> Result<Ppk, PuttyError> {
|
||||
if self.encryption.is_some() {
|
||||
return Err(PuttyError::AlreadyEncrypted);
|
||||
}
|
||||
|
||||
let ppk = match self.version {
|
||||
PpkVersionKey::V2 => {
|
||||
let key_material = kdf::derive_key_material_v2(passphrase)?;
|
||||
|
||||
let mut private_key = ppk_aes::make_padding(self.private_key.clone(), rng);
|
||||
let mac = self.calculate_mac_v2(passphrase, &private_key, PpkEncryptionValue::Aes256Cbc)?;
|
||||
ppk_aes::encrypt(&mut private_key, key_material.key(), KeyMaterialV2::iv())?;
|
||||
|
||||
Ppk {
|
||||
version: self.version,
|
||||
algorithm: self.algorithm,
|
||||
encryption: Some(PpkEncryptionKind::Aes256CbcV2),
|
||||
comment: self.comment.clone(),
|
||||
public_key: self.public_key.clone(),
|
||||
private_key,
|
||||
mac,
|
||||
}
|
||||
}
|
||||
PpkVersionKey::V3 => {
|
||||
let mut argon2_salt = vec![0u8; config.argon2_salt_size as usize];
|
||||
rng.fill_bytes(&mut argon2_salt);
|
||||
let argon2_params = Argon2Params {
|
||||
flavor: config.argon2_flavour,
|
||||
memory: config.argon2_memory,
|
||||
passes: config.argon2_passes,
|
||||
parallelism: config.argon2_parallelism,
|
||||
salt: argon2_salt,
|
||||
};
|
||||
|
||||
let key_material = kdf::derive_key_material_v3(&argon2_params, passphrase)?;
|
||||
|
||||
let mut private_key = ppk_aes::make_padding(self.private_key.clone(), rng);
|
||||
let mac =
|
||||
self.calculate_mac_v3(key_material.hmac_key(), &private_key, PpkEncryptionValue::Aes256Cbc)?;
|
||||
ppk_aes::encrypt(&mut private_key, key_material.key(), key_material.iv())?;
|
||||
|
||||
Ppk {
|
||||
version: self.version,
|
||||
algorithm: self.algorithm,
|
||||
encryption: Some(PpkEncryptionKind::Aes256CbcV3(argon2_params)),
|
||||
comment: self.comment.clone(),
|
||||
public_key: self.public_key.clone(),
|
||||
private_key,
|
||||
mac,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ppk)
|
||||
}
|
||||
|
||||
/// Returns PPK key decrypted with the specified passphrase
|
||||
pub fn decrypt(&self, passphrase: &str) -> Result<Self, PuttyError> {
|
||||
let encrytion = if let Some(encryption) = &self.encryption {
|
||||
encryption
|
||||
} else {
|
||||
return Err(PuttyError::AlreadyDecrypted);
|
||||
};
|
||||
|
||||
let (mut private_key, mac) = match encrytion {
|
||||
PpkEncryptionKind::Aes256CbcV2 => {
|
||||
let key_material = kdf::derive_key_material_v2(passphrase)?;
|
||||
let mut private_key = self.private_key.clone();
|
||||
ppk_aes::decrypt(&mut private_key, key_material.key(), KeyMaterialV2::iv())?;
|
||||
let mac = self.calculate_mac_v2(passphrase, &private_key, PpkEncryptionValue::Aes256Cbc)?;
|
||||
(private_key, mac)
|
||||
}
|
||||
PpkEncryptionKind::Aes256CbcV3(argon2_params) => {
|
||||
let key_material = kdf::derive_key_material_v3(argon2_params, passphrase)?;
|
||||
let mut private_key = self.private_key.clone();
|
||||
ppk_aes::decrypt(&mut private_key, key_material.key(), key_material.iv())?;
|
||||
let mac =
|
||||
self.calculate_mac_v3(key_material.hmac_key(), &private_key, PpkEncryptionValue::Aes256Cbc)?;
|
||||
(private_key, mac)
|
||||
}
|
||||
};
|
||||
|
||||
// Verify MAC
|
||||
if mac.as_slice() != self.mac.as_slice() {
|
||||
return Err(PuttyError::MacValidation);
|
||||
}
|
||||
|
||||
// Truncate private key padding if any
|
||||
let truncated_size = {
|
||||
let mut mpint_cursor = private_key.as_slice();
|
||||
for _ in 0..self.algorithm.key_mpint_values_count() {
|
||||
// NOTE: Bytes and mpint stored the same way, therefore to avoid BigUint
|
||||
// construction we can just read the bytes (we discard them either way)
|
||||
let _value = mpint_cursor.read_ssh_bytes()?;
|
||||
}
|
||||
|
||||
private_key.len().wrapping_sub(mpint_cursor.len())
|
||||
};
|
||||
|
||||
private_key.truncate(truncated_size);
|
||||
|
||||
let mac = self.calculate_unencrypted_mac(private_key.as_slice())?;
|
||||
|
||||
let ppk = Ppk {
|
||||
version: self.version,
|
||||
algorithm: self.algorithm,
|
||||
encryption: None,
|
||||
comment: self.comment.clone(),
|
||||
public_key: self.public_key.clone(),
|
||||
private_key,
|
||||
mac,
|
||||
};
|
||||
|
||||
Ok(ppk)
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
//! Key derivation utilities for PPK files.
|
||||
|
||||
use crate::putty::key_value::Argon2FlavourValue;
|
||||
use crate::putty::ppk::{PuttyError, aes as ppk_aes};
|
||||
|
||||
use digest::Digest;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
const SHA256_DIGEST_SIZE: usize = 32;
|
||||
const SHA1_DIGEST_SIZE: usize = 20;
|
||||
|
||||
pub const MAC_SIZE_V3: usize = SHA256_DIGEST_SIZE;
|
||||
|
||||
/// Argon2 key derivation function parameters.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Argon2Params {
|
||||
pub flavor: Argon2FlavourValue,
|
||||
pub memory: u32,
|
||||
pub passes: u32,
|
||||
pub parallelism: u32,
|
||||
pub salt: Vec<u8>,
|
||||
}
|
||||
|
||||
const V3_KEY_MATERIAL_SIZE: usize = ppk_aes::KEY_SIZE // AES key
|
||||
+ ppk_aes::BLOCK_SIZE // AES IV
|
||||
+ MAC_SIZE_V3; // HMAC key
|
||||
|
||||
pub(crate) struct KeyMaterialV3 {
|
||||
key: Zeroizing<[u8; ppk_aes::KEY_SIZE]>,
|
||||
iv: Zeroizing<[u8; ppk_aes::BLOCK_SIZE]>,
|
||||
hmac_key: Zeroizing<[u8; MAC_SIZE_V3]>,
|
||||
}
|
||||
|
||||
impl KeyMaterialV3 {
|
||||
pub fn key(&self) -> &[u8] {
|
||||
self.key.as_ref()
|
||||
}
|
||||
|
||||
pub fn iv(&self) -> &[u8] {
|
||||
self.iv.as_ref()
|
||||
}
|
||||
|
||||
pub fn hmac_key(&self) -> &[u8] {
|
||||
self.hmac_key.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct KeyMaterialV2 {
|
||||
key: Zeroizing<[u8; ppk_aes::KEY_SIZE]>,
|
||||
}
|
||||
|
||||
impl KeyMaterialV2 {
|
||||
pub fn key(&self) -> &[u8] {
|
||||
self.key.as_ref()
|
||||
}
|
||||
|
||||
pub fn iv() -> &'static [u8; ppk_aes::BLOCK_SIZE] {
|
||||
&[0u8; ppk_aes::BLOCK_SIZE]
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn derive_key_material_v2(passphrase: &str) -> Result<KeyMaterialV2, PuttyError> {
|
||||
let tagged_hash = |tag: u32| -> [u8; SHA1_DIGEST_SIZE] {
|
||||
let mut digest = sha1::Sha1::new();
|
||||
digest.update(tag.to_be_bytes());
|
||||
digest.update(passphrase.as_bytes());
|
||||
digest.finalize().into()
|
||||
};
|
||||
|
||||
let hash1 = tagged_hash(0);
|
||||
let hash2 = tagged_hash(1);
|
||||
|
||||
let mut key = [0u8; ppk_aes::KEY_SIZE];
|
||||
key[..SHA1_DIGEST_SIZE].copy_from_slice(&hash1[..]);
|
||||
key[SHA1_DIGEST_SIZE..].copy_from_slice(&hash2[..ppk_aes::KEY_SIZE - SHA1_DIGEST_SIZE]);
|
||||
|
||||
Ok(KeyMaterialV2 { key: key.into() })
|
||||
}
|
||||
|
||||
pub(crate) fn derive_key_material_v3(
|
||||
argon2_params: &Argon2Params,
|
||||
passphrase: &str,
|
||||
) -> Result<KeyMaterialV3, PuttyError> {
|
||||
let mut key_material = [0u8; V3_KEY_MATERIAL_SIZE];
|
||||
|
||||
let kdf = argon2::Argon2::new(
|
||||
argon2_params.flavor.into(),
|
||||
argon2::Version::V0x13,
|
||||
argon2::Params::new(
|
||||
argon2_params.memory,
|
||||
argon2_params.passes,
|
||||
argon2_params.parallelism,
|
||||
Some(V3_KEY_MATERIAL_SIZE),
|
||||
)
|
||||
.map_err(|_| PuttyError::Argon2)?,
|
||||
);
|
||||
|
||||
kdf.hash_password_into(passphrase.as_bytes(), &argon2_params.salt, &mut key_material)
|
||||
.map_err(|_| PuttyError::Argon2)?;
|
||||
|
||||
let key_material = key_material.as_ref();
|
||||
|
||||
const IV_OFFSET: usize = ppk_aes::KEY_SIZE;
|
||||
const HMAC_KEY_OFFSET: usize = IV_OFFSET + ppk_aes::BLOCK_SIZE;
|
||||
|
||||
let mut key = [0u8; ppk_aes::KEY_SIZE];
|
||||
key.copy_from_slice(&key_material[..ppk_aes::KEY_SIZE]);
|
||||
|
||||
let mut iv = [0u8; ppk_aes::BLOCK_SIZE];
|
||||
iv.copy_from_slice(&key_material[IV_OFFSET..IV_OFFSET + ppk_aes::BLOCK_SIZE]);
|
||||
|
||||
let mut hmac_key = [0u8; MAC_SIZE_V3];
|
||||
hmac_key.copy_from_slice(&key_material[HMAC_KEY_OFFSET..]);
|
||||
|
||||
Ok(KeyMaterialV3 {
|
||||
key: key.into(),
|
||||
iv: iv.into(),
|
||||
hmac_key: hmac_key.into(),
|
||||
})
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
//! PPK MAC calculation functions
|
||||
|
||||
use crate::putty::key_value::{PpkEncryptionValue, PpkLiteral, PpkVersionKey};
|
||||
use crate::putty::{Ppk, PuttyError};
|
||||
|
||||
use digest::Digest;
|
||||
use hmac::{KeyInit, Mac};
|
||||
|
||||
impl Ppk {
|
||||
pub(super) fn calculate_mac_v3(
|
||||
&self,
|
||||
mac_key: &[u8],
|
||||
private_key_data: &[u8],
|
||||
encryption: PpkEncryptionValue,
|
||||
) -> Result<Vec<u8>, PuttyError> {
|
||||
let mut hmac = hmac::Hmac::<sha2::Sha256>::new_from_slice(mac_key).map_err(|_| PuttyError::MacValidation)?;
|
||||
|
||||
let mut hash_bytes = |data: &[u8]| -> Result<(), PuttyError> {
|
||||
hmac.update(
|
||||
&u32::try_from(data.len())
|
||||
.map_err(|_| PuttyError::MacValidation)?
|
||||
.to_be_bytes(),
|
||||
);
|
||||
hmac.update(data);
|
||||
Ok(())
|
||||
};
|
||||
|
||||
hash_bytes(self.algorithm.as_static_str().as_bytes())?;
|
||||
hash_bytes(encryption.as_static_str().as_bytes())?;
|
||||
hash_bytes(self.comment.as_bytes())?;
|
||||
hash_bytes(&self.public_key)?;
|
||||
hash_bytes(private_key_data)?;
|
||||
|
||||
let mac = hmac.finalize().into_bytes().to_vec();
|
||||
Ok(mac)
|
||||
}
|
||||
|
||||
pub(super) fn calculate_mac_v2(
|
||||
&self,
|
||||
passphrase: &str,
|
||||
private_key_data: &[u8],
|
||||
encryption: PpkEncryptionValue,
|
||||
) -> Result<Vec<u8>, PuttyError> {
|
||||
let mac_key = {
|
||||
let mut digest = sha1::Sha1::new();
|
||||
digest.update(b"putty-private-key-file-mac-key");
|
||||
digest.update(passphrase.as_bytes());
|
||||
digest.finalize()
|
||||
};
|
||||
|
||||
let mut hmac = hmac::Hmac::<sha1::Sha1>::new_from_slice(&mac_key).map_err(|_| PuttyError::MacValidation)?;
|
||||
|
||||
let mut hash_bytes = |data: &[u8]| -> Result<(), PuttyError> {
|
||||
hmac.update(
|
||||
&u32::try_from(data.len())
|
||||
.map_err(|_| PuttyError::MacValidation)?
|
||||
.to_be_bytes(),
|
||||
);
|
||||
hmac.update(data);
|
||||
Ok(())
|
||||
};
|
||||
|
||||
hash_bytes(self.algorithm.as_static_str().as_bytes())?;
|
||||
hash_bytes(encryption.as_static_str().as_bytes())?;
|
||||
hash_bytes(self.comment.as_bytes())?;
|
||||
hash_bytes(&self.public_key)?;
|
||||
hash_bytes(private_key_data)?;
|
||||
|
||||
let mac = hmac.finalize().into_bytes().to_vec();
|
||||
Ok(mac)
|
||||
}
|
||||
|
||||
pub(super) fn calculate_unencrypted_mac(&self, private_key_data: &[u8]) -> Result<Vec<u8>, PuttyError> {
|
||||
match self.version {
|
||||
PpkVersionKey::V2 => self.calculate_mac_v2("", private_key_data, PpkEncryptionValue::None),
|
||||
PpkVersionKey::V3 => self.calculate_mac_v3(&[], private_key_data, PpkEncryptionValue::None),
|
||||
}
|
||||
}
|
||||
}
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
mod aes;
|
||||
mod encoding;
|
||||
mod encryption;
|
||||
mod kdf;
|
||||
mod mac;
|
||||
|
||||
use crate::key::{EcCurve, PrivateKey, PublicKey};
|
||||
use crate::putty::PuttyError;
|
||||
use crate::putty::key_value::{PpkKeyAlgorithmValue, PpkVersionKey};
|
||||
use crate::putty::private_key::{PuttyBasePrivateKey, PuttyPrivateKey};
|
||||
use crate::putty::public_key::{PuttyBasePublicKey, PuttyPublicKey};
|
||||
use crate::ssh::SshPrivateKey;
|
||||
|
||||
use self::encryption::PpkEncryptionKind;
|
||||
|
||||
pub use encryption::{PpkEncryptionConfig, PpkEncryptionConfigBuilder};
|
||||
pub use kdf::Argon2Params;
|
||||
|
||||
/// PuTTY Private Key (PPK) format.
|
||||
///
|
||||
/// ### Functionality
|
||||
/// - Generation of new keys.
|
||||
/// - Conversion to/from OpenSSH format.
|
||||
/// - Encoding/decoding to/from string.
|
||||
/// - Version upgrade/downgrade.
|
||||
///
|
||||
/// ### Usage notes
|
||||
/// - Ppk structure is immutable. All operations that modify the key return a new instance.
|
||||
/// - When input file is encrypted, all operations with the private key will be unavailable until
|
||||
/// ppk is decrypted via [`Ppk::decrypt`].
|
||||
/// - Newly generated keys are always unencrypted. They should be encrypted via [`Ppk::encrypt`]
|
||||
/// when required
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Ppk {
|
||||
version: PpkVersionKey,
|
||||
algorithm: PpkKeyAlgorithmValue,
|
||||
encryption: Option<PpkEncryptionKind>,
|
||||
comment: String,
|
||||
public_key: Vec<u8>,
|
||||
private_key: Vec<u8>,
|
||||
mac: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Ppk {
|
||||
pub fn generate_rsa(bits: usize, comment: Option<&str>) -> Result<Self, PuttyError> {
|
||||
let ssh_key = SshPrivateKey::generate_rsa(bits, None, comment.map(From::from))?;
|
||||
Self::from_openssh_private_key(&ssh_key)
|
||||
}
|
||||
|
||||
pub fn generate_ec(curve: EcCurve, comment: Option<&str>) -> Result<Self, PuttyError> {
|
||||
let ssh_key = SshPrivateKey::generate_ec(curve, None, comment.map(From::from))?;
|
||||
Self::from_openssh_private_key(&ssh_key)
|
||||
}
|
||||
|
||||
pub fn generate_ed25519(comment: Option<&str>) -> Result<Self, PuttyError> {
|
||||
let ssh_key = SshPrivateKey::generate_ed25519(None, comment.map(From::from))?;
|
||||
Self::from_openssh_private_key(&ssh_key)
|
||||
}
|
||||
|
||||
/// Converts the OpenSSH private key to a PPK key.
|
||||
pub fn from_openssh_private_key(key: &SshPrivateKey) -> Result<Self, PuttyError> {
|
||||
let PuttyPrivateKey { base, comment }: PuttyPrivateKey = PuttyPrivateKey::from_openssh(key)?;
|
||||
|
||||
let mut ppk = Ppk {
|
||||
version: PpkVersionKey::V3,
|
||||
algorithm: base.algorithm,
|
||||
encryption: None,
|
||||
comment,
|
||||
public_key: base.public_key.data,
|
||||
private_key: base.data,
|
||||
mac: vec![],
|
||||
};
|
||||
|
||||
ppk.mac = ppk.calculate_unencrypted_mac(ppk.private_key.as_slice())?;
|
||||
|
||||
Ok(ppk)
|
||||
}
|
||||
|
||||
/// Converts the PPK key to an OpenSSH private key (with or without encryption).
|
||||
pub fn to_openssh_private_key(&self, passphrase: Option<&str>) -> Result<SshPrivateKey, PuttyError> {
|
||||
if self.is_encrypted() {
|
||||
return Err(PuttyError::Encrypted);
|
||||
}
|
||||
|
||||
let base = PuttyBasePrivateKey {
|
||||
algorithm: self.algorithm,
|
||||
public_key: PuttyBasePublicKey {
|
||||
data: self.public_key.clone(),
|
||||
},
|
||||
data: self.private_key.clone(),
|
||||
};
|
||||
|
||||
let key = PuttyPrivateKey {
|
||||
base,
|
||||
comment: self.comment.clone(),
|
||||
};
|
||||
|
||||
key.to_openssh(passphrase)
|
||||
}
|
||||
|
||||
/// Returns PPK public key.
|
||||
pub fn public_key(&self) -> Result<PublicKey, PuttyError> {
|
||||
PuttyBasePublicKey {
|
||||
data: self.public_key.clone(),
|
||||
}
|
||||
.to_inner_key()
|
||||
}
|
||||
|
||||
/// Returns PPK private key.
|
||||
pub fn private_key(&self) -> Result<PrivateKey, PuttyError> {
|
||||
if self.is_encrypted() {
|
||||
return Err(PuttyError::Encrypted);
|
||||
}
|
||||
|
||||
PuttyBasePrivateKey {
|
||||
algorithm: self.algorithm,
|
||||
public_key: PuttyBasePublicKey {
|
||||
data: self.public_key.clone(),
|
||||
},
|
||||
data: self.private_key.clone(),
|
||||
}
|
||||
.to_inner_key()
|
||||
}
|
||||
|
||||
/// Returns extracted public key in PuTTY format.
|
||||
pub fn extract_putty_public_key(&self) -> Result<PuttyPublicKey, PuttyError> {
|
||||
Ok(PuttyPublicKey {
|
||||
base: PuttyBasePublicKey {
|
||||
data: self.public_key.clone(),
|
||||
},
|
||||
comment: self.comment.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a new PPK key instance with a different comment.
|
||||
pub fn with_comment(&self, comment: &str) -> Result<Self, PuttyError> {
|
||||
if self.is_encrypted() {
|
||||
// We need to decrypt the key to change the comment (MAC should be recalculated).
|
||||
return Err(PuttyError::Encrypted);
|
||||
}
|
||||
|
||||
let mut ppk = Self {
|
||||
comment: comment.to_string(),
|
||||
..self.clone()
|
||||
};
|
||||
|
||||
ppk.mac = ppk.calculate_unencrypted_mac(ppk.private_key.as_slice())?;
|
||||
|
||||
Ok(ppk)
|
||||
}
|
||||
|
||||
/// Returns the version of the PPK file format.
|
||||
pub fn version(&self) -> PpkVersionKey {
|
||||
self.version
|
||||
}
|
||||
|
||||
/// Returns the key algorithm.
|
||||
pub fn algorithm(&self) -> PpkKeyAlgorithmValue {
|
||||
self.algorithm
|
||||
}
|
||||
|
||||
/// Returns key comment.
|
||||
pub fn comment(&self) -> &str {
|
||||
&self.comment
|
||||
}
|
||||
|
||||
/// Returns new PPK kew with the specified format version.
|
||||
///
|
||||
/// NOTE: `PpkVersionKey::V2` is considered insecure and should not be used for new keys in
|
||||
/// normal circumstances.
|
||||
pub fn to_version(&self, version: PpkVersionKey) -> Result<Ppk, PuttyError> {
|
||||
if self.is_encrypted() {
|
||||
return Err(PuttyError::Encrypted);
|
||||
}
|
||||
|
||||
let ppk = Ppk {
|
||||
version,
|
||||
algorithm: self.algorithm,
|
||||
encryption: None,
|
||||
comment: self.comment.clone(),
|
||||
public_key: self.public_key.clone(),
|
||||
private_key: self.private_key.clone(),
|
||||
mac: self.calculate_unencrypted_mac(self.private_key.as_slice())?,
|
||||
};
|
||||
|
||||
Ok(ppk)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rstest::rstest;
|
||||
|
||||
use picky_test_data::{
|
||||
PUTTY_KEY_ED25519, PUTTY_KEY_ED25519_ENCRYPTED, PUTTY_KEY_ED25519_V2, PUTTY_KEY_ED25519_V2_ENCRYPTED,
|
||||
SSH_PRIVATE_KEY_EC_P256, SSH_PRIVATE_KEY_EC_P384, SSH_PRIVATE_KEY_EC_P521, SSH_PRIVATE_KEY_ED25519,
|
||||
SSH_PRIVATE_KEY_RSA,
|
||||
};
|
||||
|
||||
#[rstest]
|
||||
#[case(PUTTY_KEY_ED25519)]
|
||||
#[case(PUTTY_KEY_ED25519_ENCRYPTED)]
|
||||
#[case(PUTTY_KEY_ED25519_V2)]
|
||||
#[case(PUTTY_KEY_ED25519_V2_ENCRYPTED)]
|
||||
fn ppk_encode_decode_roundtrip(#[case] input: &str) {
|
||||
let key: Ppk = input.parse().unwrap();
|
||||
let encoded = key.to_string().unwrap();
|
||||
|
||||
assert_eq!(encoded, input);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(PUTTY_KEY_ED25519_ENCRYPTED, PUTTY_KEY_ED25519)]
|
||||
#[case(PUTTY_KEY_ED25519_V2_ENCRYPTED, PUTTY_KEY_ED25519_V2)]
|
||||
fn decrypt_produces_same_key_as_puttygen(#[case] encrypted: &str, #[case] decrypted: &str) {
|
||||
let mut key: Ppk = encrypted.parse().unwrap();
|
||||
assert!(key.is_encrypted());
|
||||
key = key.decrypt("test").unwrap();
|
||||
|
||||
let encoded = key.to_string().unwrap();
|
||||
assert_eq!(encoded, decrypted);
|
||||
assert!(!key.is_encrypted());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(PUTTY_KEY_ED25519, PpkVersionKey::V3, "eddsa-key-20240414")]
|
||||
#[case(PUTTY_KEY_ED25519_V2, PpkVersionKey::V2, "ed25519-key-20240418")]
|
||||
fn encrypt_decrypt_roundtrip(#[case] input: &str, #[case] version: PpkVersionKey, #[case] comment: &str) {
|
||||
let mut key: Ppk = input.parse().unwrap();
|
||||
key = key.encrypt("test", Default::default()).unwrap();
|
||||
assert!(key.is_encrypted());
|
||||
key = key.decrypt("test").unwrap();
|
||||
assert_eq!(key.to_string().unwrap(), input);
|
||||
assert!(!key.is_encrypted());
|
||||
assert_eq!(key.version(), version);
|
||||
assert_eq!(key.algorithm(), PpkKeyAlgorithmValue::Ed25519);
|
||||
assert_eq!(key.comment(), comment);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(SSH_PRIVATE_KEY_RSA)]
|
||||
#[case(SSH_PRIVATE_KEY_EC_P256)]
|
||||
#[case(SSH_PRIVATE_KEY_EC_P384)]
|
||||
#[case(SSH_PRIVATE_KEY_EC_P521)]
|
||||
#[case(SSH_PRIVATE_KEY_ED25519)]
|
||||
fn test_openssh_roundtrip(#[case] input: &str) {
|
||||
let ssh_key = SshPrivateKey::from_pem_str(input, None).unwrap();
|
||||
let key = Ppk::from_openssh_private_key(&ssh_key).unwrap();
|
||||
let mut ssh_key2 = key.to_openssh_private_key(None).unwrap();
|
||||
|
||||
// Check is re-generated when new ssh is created from scratch
|
||||
ssh_key2.check = ssh_key.check;
|
||||
|
||||
let ssh_key_str = ssh_key2.to_string().unwrap();
|
||||
assert_eq!(ssh_key_str, input);
|
||||
}
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
use crate::key::ec::{EcdsaKeypair, EcdsaPublicKey, NamedEcCurve};
|
||||
use crate::key::ed::{EdKeypair, EdPublicKey, NamedEdAlgorithm};
|
||||
use crate::key::{EcCurve, EdAlgorithm, PrivateKey};
|
||||
use crate::putty::PuttyError;
|
||||
use crate::putty::key_value::PpkKeyAlgorithmValue;
|
||||
use crate::putty::public_key::PuttyBasePublicKey;
|
||||
use crate::ssh::SshPrivateKey;
|
||||
use crate::ssh::decode::SshReadExt;
|
||||
use crate::ssh::encode::SshWriteExt;
|
||||
use crate::ssh::private_key::SshBasePrivateKey;
|
||||
use crate::ssh::public_key::SshBasePublicKey;
|
||||
use crypto_bigint::BoxedUint;
|
||||
use rsa::traits::{PrivateKeyParts, PublicKeyParts};
|
||||
use rsa::{RsaPrivateKey, RsaPublicKey};
|
||||
|
||||
/// PuTTY private key wrapper
|
||||
pub(crate) struct PuttyPrivateKey {
|
||||
pub(crate) base: PuttyBasePrivateKey,
|
||||
pub(crate) comment: String,
|
||||
}
|
||||
|
||||
impl PuttyPrivateKey {
|
||||
pub fn from_openssh(key: &SshPrivateKey) -> Result<Self, PuttyError> {
|
||||
let base = PuttyBasePrivateKey::from_openssh(&key.base_key)?;
|
||||
let comment = key.comment.clone();
|
||||
|
||||
Ok(Self { base, comment })
|
||||
}
|
||||
|
||||
/// Converts the key to an OpenSSH key (with or without encryption)
|
||||
pub fn to_openssh(&self, passphrase: Option<&str>) -> Result<SshPrivateKey, PuttyError> {
|
||||
let base = self.base.to_openssh()?;
|
||||
let comment = if self.comment.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.comment.clone())
|
||||
};
|
||||
|
||||
let key = match base {
|
||||
SshBasePrivateKey::Rsa(key) => key,
|
||||
SshBasePrivateKey::Ec(key) => key,
|
||||
SshBasePrivateKey::Ed(key) => key,
|
||||
SshBasePrivateKey::SkEcdsaSha2NistP256 { .. } | SshBasePrivateKey::SkEd25519 { .. } => {
|
||||
return Err(PuttyError::NotSupported { feature: "SK keys" });
|
||||
}
|
||||
};
|
||||
|
||||
Ok(SshPrivateKey::h_picky_private_key_to_ssh_private_key(
|
||||
key,
|
||||
passphrase.map(From::from),
|
||||
comment,
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PuttyBasePrivateKey {
|
||||
pub(crate) algorithm: PpkKeyAlgorithmValue,
|
||||
pub(crate) public_key: PuttyBasePublicKey,
|
||||
pub(crate) data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PuttyBasePrivateKey {
|
||||
pub fn from_openssh(key: &SshBasePrivateKey) -> Result<Self, PuttyError> {
|
||||
let mut data = Vec::new();
|
||||
let cursor = &mut data;
|
||||
|
||||
match key {
|
||||
SshBasePrivateKey::SkEcdsaSha2NistP256 { .. } | SshBasePrivateKey::SkEd25519 { .. } => {
|
||||
// Putty does not support SK keys
|
||||
Err(PuttyError::NotSupported { feature: "SK keys" })
|
||||
}
|
||||
SshBasePrivateKey::Rsa(key) => {
|
||||
let mut rsa_key = RsaPrivateKey::try_from(key)?;
|
||||
|
||||
cursor.write_ssh_mpint(rsa_key.d())?;
|
||||
if rsa_key.primes().len() != 2 {
|
||||
return Err(PuttyError::RsaInvalidPrimesCount {
|
||||
count: rsa_key.primes().len(),
|
||||
});
|
||||
}
|
||||
cursor.write_ssh_mpint(&rsa_key.primes()[0])?;
|
||||
cursor.write_ssh_mpint(&rsa_key.primes()[1])?;
|
||||
|
||||
rsa_key.precompute().map_err(|_| PuttyError::RsaPrecompute)?;
|
||||
let qinv = rsa_key
|
||||
.qinv()
|
||||
.expect("BUG: should be precomuted above")
|
||||
.retrieve()
|
||||
.to_be_bytes_trimmed_vartime();
|
||||
cursor.write_ssh_bytes(&qinv)?;
|
||||
|
||||
let ssh_public_key = SshBasePublicKey::Rsa(key.to_public_key()?);
|
||||
let public_key = PuttyBasePublicKey::from_openssh(&ssh_public_key)?;
|
||||
|
||||
Ok(Self {
|
||||
algorithm: PpkKeyAlgorithmValue::Rsa,
|
||||
data,
|
||||
public_key,
|
||||
})
|
||||
}
|
||||
SshBasePrivateKey::Ec(key) => {
|
||||
let ec_key = EcdsaKeypair::try_from(key)?;
|
||||
|
||||
let secret = BoxedUint::from_be_slice_vartime(ec_key.secret());
|
||||
cursor.write_ssh_mpint(&secret)?;
|
||||
|
||||
let algorithm = match ec_key.curve() {
|
||||
NamedEcCurve::Known(EcCurve::NistP256) => PpkKeyAlgorithmValue::EcdsaSha2Nistp256,
|
||||
NamedEcCurve::Known(EcCurve::NistP384) => PpkKeyAlgorithmValue::EcdsaSha2Nistp384,
|
||||
NamedEcCurve::Known(EcCurve::NistP521) => PpkKeyAlgorithmValue::EcdsaSha2Nistp521,
|
||||
_ => {
|
||||
return Err(PuttyError::NotSupported {
|
||||
feature: "unknown EC curve",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let ssh_public_key = SshBasePublicKey::Ec(key.to_public_key()?);
|
||||
let public_key = PuttyBasePublicKey::from_openssh(&ssh_public_key)?;
|
||||
|
||||
Ok(Self {
|
||||
algorithm,
|
||||
data,
|
||||
public_key,
|
||||
})
|
||||
}
|
||||
SshBasePrivateKey::Ed(key) => {
|
||||
let ed_key = EdKeypair::try_from(key)?;
|
||||
|
||||
cursor.write_ssh_mpint(&BoxedUint::from_be_slice_vartime(ed_key.secret()))?;
|
||||
|
||||
let algorithm = match ed_key.algorithm() {
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::Ed25519) => PpkKeyAlgorithmValue::Ed25519,
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::X25519) => {
|
||||
return Err(PuttyError::NotSupported { feature: "X25519 keys" });
|
||||
}
|
||||
_ => {
|
||||
return Err(PuttyError::NotSupported {
|
||||
feature: "unknown EdDSA algorithm",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let ssh_public_key = SshBasePublicKey::Ed(key.to_public_key()?);
|
||||
let public_key = PuttyBasePublicKey::from_openssh(&ssh_public_key)?;
|
||||
|
||||
Ok(Self {
|
||||
algorithm,
|
||||
data,
|
||||
public_key,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_openssh(&self) -> Result<SshBasePrivateKey, PuttyError> {
|
||||
let ssh_public_key = self.public_key.to_openssh()?;
|
||||
let mut data = self.data.as_slice();
|
||||
|
||||
match self.algorithm {
|
||||
PpkKeyAlgorithmValue::Rsa => {
|
||||
let public = match &ssh_public_key {
|
||||
SshBasePublicKey::Rsa(rsa) => RsaPublicKey::try_from(rsa)?,
|
||||
_ => return Err(PuttyError::PublicAndPrivateKeyMismatch),
|
||||
};
|
||||
|
||||
let d = data.read_ssh_mpint()?;
|
||||
let p1 = data.read_ssh_mpint()?;
|
||||
let p2 = data.read_ssh_mpint()?;
|
||||
let _qinv = data.read_ssh_mpint()?;
|
||||
|
||||
let private_key = PrivateKey::from_rsa_components(public.n(), public.e(), &d, &[p1, p2])?;
|
||||
|
||||
Ok(SshBasePrivateKey::Rsa(private_key))
|
||||
}
|
||||
PpkKeyAlgorithmValue::EcdsaSha2Nistp256
|
||||
| PpkKeyAlgorithmValue::EcdsaSha2Nistp384
|
||||
| PpkKeyAlgorithmValue::EcdsaSha2Nistp521 => {
|
||||
let public = match &ssh_public_key {
|
||||
SshBasePublicKey::Ec(rsa) => EcdsaPublicKey::try_from(rsa)?,
|
||||
_ => return Err(PuttyError::PublicAndPrivateKeyMismatch),
|
||||
};
|
||||
|
||||
let secret = data.read_ssh_mpint()?;
|
||||
|
||||
let curve = match self.algorithm {
|
||||
PpkKeyAlgorithmValue::EcdsaSha2Nistp256 => NamedEcCurve::Known(EcCurve::NistP256),
|
||||
PpkKeyAlgorithmValue::EcdsaSha2Nistp384 => NamedEcCurve::Known(EcCurve::NistP384),
|
||||
PpkKeyAlgorithmValue::EcdsaSha2Nistp521 => NamedEcCurve::Known(EcCurve::NistP521),
|
||||
_ => unreachable!("BUG: algorithm is checked above"),
|
||||
};
|
||||
|
||||
let private_key = PrivateKey::from_ec_encoded_components(
|
||||
curve.into(),
|
||||
&secret.to_be_bytes_trimmed_vartime(),
|
||||
Some(public.encoded_point()),
|
||||
);
|
||||
|
||||
Ok(SshBasePrivateKey::Ec(private_key))
|
||||
}
|
||||
PpkKeyAlgorithmValue::Ed25519 => {
|
||||
let public = match &ssh_public_key {
|
||||
SshBasePublicKey::Ed(rsa) => EdPublicKey::try_from(rsa)?,
|
||||
_ => return Err(PuttyError::PublicAndPrivateKeyMismatch),
|
||||
};
|
||||
|
||||
let secret = data.read_ssh_mpint()?;
|
||||
|
||||
let private_key = PrivateKey::from_ed_encoded_components(
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::Ed25519).into(),
|
||||
&secret.to_be_bytes_trimmed_vartime(),
|
||||
Some(public.data()),
|
||||
);
|
||||
|
||||
Ok(SshBasePrivateKey::Ed(private_key))
|
||||
}
|
||||
_ => Err(PuttyError::NotSupported {
|
||||
feature: "unsupported key algorithm",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_inner_key(&self) -> Result<PrivateKey, PuttyError> {
|
||||
let inner = match self.to_openssh()? {
|
||||
SshBasePrivateKey::Rsa(key) => key,
|
||||
SshBasePrivateKey::Ec(key) => key,
|
||||
SshBasePrivateKey::Ed(key) => key,
|
||||
SshBasePrivateKey::SkEcdsaSha2NistP256 { .. } | SshBasePrivateKey::SkEd25519 { .. } => {
|
||||
return Err(PuttyError::NotSupported { feature: "SK keys" });
|
||||
}
|
||||
};
|
||||
|
||||
Ok(inner)
|
||||
}
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
use crate::key::PublicKey;
|
||||
use crate::putty::PuttyError;
|
||||
use crate::ssh::SshPublicKey;
|
||||
use crate::ssh::decode::SshComplexTypeDecode;
|
||||
use crate::ssh::encode::SshComplexTypeEncode;
|
||||
use crate::ssh::public_key::SshBasePublicKey;
|
||||
use std::str::FromStr;
|
||||
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_ENGINE;
|
||||
|
||||
const PUTTY_PUBKEY_HEADER: &str = "---- BEGIN SSH2 PUBLIC KEY ----";
|
||||
const PUTTY_PUBKEY_FOOTER: &str = "---- END SSH2 PUBLIC KEY ----";
|
||||
|
||||
/// PuTTY public key format.
|
||||
///
|
||||
/// ### Functionality:
|
||||
/// - Conversion to/from OpenSSH format.
|
||||
/// - Encoding/decoding to/from string.
|
||||
/// - Could be extracted from [`crate::putty::Ppk`] private keys.
|
||||
///
|
||||
/// ### Notes
|
||||
/// - Although top-level containeris similar to PEM, it is not compatible with it because of
|
||||
/// additional comment field after the header.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PuttyPublicKey {
|
||||
pub(crate) base: PuttyBasePublicKey,
|
||||
pub(crate) comment: String,
|
||||
}
|
||||
|
||||
impl PuttyPublicKey {
|
||||
/// Converts an OpenSSH public key to a PuTTY public key.
|
||||
pub fn from_openssh(key: &SshPublicKey) -> Result<Self, PuttyError> {
|
||||
let base = PuttyBasePublicKey::from_openssh(&key.inner_key)?;
|
||||
|
||||
Ok(Self {
|
||||
base,
|
||||
comment: key.comment.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Converts the key to an OpenSSH public key.
|
||||
pub fn to_openssh(&self) -> Result<SshPublicKey, PuttyError> {
|
||||
let base = self.base.to_openssh()?;
|
||||
Ok(SshPublicKey {
|
||||
inner_key: base,
|
||||
comment: self.comment.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns key comment.
|
||||
pub fn comment(&self) -> &str {
|
||||
&self.comment
|
||||
}
|
||||
|
||||
/// Returns a new public key instance with a different comment.
|
||||
pub fn with_comment(&self, comment: &str) -> Self {
|
||||
Self {
|
||||
comment: comment.to_string(),
|
||||
..self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses and returns the inner key as standard picky key type.
|
||||
pub fn to_inner_key(&self) -> Result<PublicKey, PuttyError> {
|
||||
self.base.to_inner_key()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PuttyPublicKey {
|
||||
// False positive, clippy does not take into account that [`String::replace`] requires both
|
||||
// arguments to be the same type, e.g. we can't use `&str` as a replacement for `char`.
|
||||
#[allow(clippy::single_char_pattern)]
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
const MAX_CHARS_PER_LINE: usize = 64;
|
||||
const LINE_END: &str = "\r\n";
|
||||
|
||||
let encoded_key = BASE64_ENGINE.encode(&self.base.data);
|
||||
let escaped_comment = self.comment.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
|
||||
let mut output = String::new();
|
||||
output.push_str(PUTTY_PUBKEY_HEADER);
|
||||
output.push_str(LINE_END);
|
||||
output.push_str("Comment: \"");
|
||||
output.push_str(&escaped_comment);
|
||||
output.push('"');
|
||||
output.push_str(LINE_END);
|
||||
|
||||
let mut value_remaining = encoded_key.as_str();
|
||||
|
||||
while !value_remaining.is_empty() {
|
||||
let line_len = value_remaining.len().min(MAX_CHARS_PER_LINE);
|
||||
let (line, remaining) = value_remaining.split_at(line_len);
|
||||
value_remaining = remaining;
|
||||
|
||||
output.push_str(line);
|
||||
output.push_str(LINE_END);
|
||||
}
|
||||
|
||||
output.push_str(PUTTY_PUBKEY_FOOTER);
|
||||
output.push_str(LINE_END);
|
||||
|
||||
f.write_str(&output)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PuttyPublicKey {
|
||||
type Err = PuttyError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut lines = s.lines();
|
||||
let header = lines.next().ok_or(PuttyError::EndOfInput)?;
|
||||
if header != PUTTY_PUBKEY_HEADER {
|
||||
return Err(PuttyError::InvalidPublicKeyContainer);
|
||||
}
|
||||
|
||||
let comment_line = lines.next().ok_or(PuttyError::EndOfInput)?;
|
||||
if !comment_line.starts_with("Comment: ") {
|
||||
return Err(PuttyError::InvalidPublicKeyComment);
|
||||
}
|
||||
|
||||
let unescaped_comment = comment_line
|
||||
.split_once('"')
|
||||
.and_then(|(_, remainder)| remainder.rsplit_once('"'))
|
||||
.map(|(comment, _)| comment)
|
||||
.ok_or(PuttyError::InvalidPublicKeyComment)?;
|
||||
|
||||
let comment = unescaped_comment.replace("\\\"", "\"").replace("\\\\", "\\");
|
||||
|
||||
let mut encoded_key = String::new();
|
||||
for line in lines {
|
||||
if line == PUTTY_PUBKEY_FOOTER {
|
||||
let decoded = BASE64_ENGINE
|
||||
.decode(encoded_key)
|
||||
.map_err(|_| PuttyError::InvalidPublicKeyData)?;
|
||||
|
||||
return Ok(PuttyPublicKey {
|
||||
base: PuttyBasePublicKey { data: decoded },
|
||||
comment,
|
||||
});
|
||||
}
|
||||
|
||||
encoded_key.push_str(line);
|
||||
}
|
||||
|
||||
Err(PuttyError::EndOfInput)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct PuttyBasePublicKey {
|
||||
pub(crate) data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PuttyBasePublicKey {
|
||||
pub fn from_openssh(key: &SshBasePublicKey) -> Result<Self, PuttyError> {
|
||||
match key {
|
||||
SshBasePublicKey::SkEcdsaSha2NistP256 { .. } | SshBasePublicKey::SkEd25519 { .. } => {
|
||||
// Putty does not support SK keys
|
||||
return Err(PuttyError::NotSupported { feature: "SK keys" });
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let mut data = Vec::new();
|
||||
SshBasePublicKey::encode(key, &mut data)?;
|
||||
|
||||
Ok(Self { data })
|
||||
}
|
||||
|
||||
pub fn to_openssh(&self) -> Result<SshBasePublicKey, PuttyError> {
|
||||
let key = SshBasePublicKey::decode(self.data.as_slice())?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
pub fn to_inner_key(&self) -> Result<PublicKey, PuttyError> {
|
||||
let inner = match self.to_openssh()? {
|
||||
SshBasePublicKey::Rsa(key) => key,
|
||||
SshBasePublicKey::Ec(key) => key,
|
||||
SshBasePublicKey::Ed(key) => key,
|
||||
SshBasePublicKey::SkEcdsaSha2NistP256 { .. } | SshBasePublicKey::SkEd25519 { .. } => {
|
||||
return Err(PuttyError::NotSupported { feature: "SK keys" });
|
||||
}
|
||||
};
|
||||
|
||||
Ok(inner)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use picky_test_data::{
|
||||
PUTTY_KEY_ED25519_PUBLIC, PUTTY_KEY_RSA_PUBLIC_EMPTY_COMMENT, PUTTY_KEY_RSA_PUBLIC_ESCAPED_COMMENT,
|
||||
SSH_PUBLIC_KEY_EC_P256, SSH_PUBLIC_KEY_EC_P384, SSH_PUBLIC_KEY_EC_P521, SSH_PUBLIC_KEY_ED25519,
|
||||
SSH_PUBLIC_KEY_RSA,
|
||||
};
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
#[case(PUTTY_KEY_ED25519_PUBLIC)]
|
||||
#[case(PUTTY_KEY_RSA_PUBLIC_EMPTY_COMMENT)]
|
||||
#[case(PUTTY_KEY_RSA_PUBLIC_ESCAPED_COMMENT)]
|
||||
fn public_key_rountrip(#[case] input: &str) {
|
||||
let key: PuttyPublicKey = input.parse().unwrap();
|
||||
let output = key.to_string();
|
||||
assert_eq!(input, output);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(SSH_PUBLIC_KEY_RSA)]
|
||||
#[case(SSH_PUBLIC_KEY_EC_P256)]
|
||||
#[case(SSH_PUBLIC_KEY_EC_P384)]
|
||||
#[case(SSH_PUBLIC_KEY_EC_P521)]
|
||||
#[case(SSH_PUBLIC_KEY_ED25519)]
|
||||
fn ssh_key_roundtrip(#[case] input: &str) {
|
||||
let ssh_key: SshPublicKey = input.parse().unwrap();
|
||||
let key = PuttyPublicKey::from_openssh(&ssh_key).unwrap();
|
||||
let ssh_key2 = key.to_openssh().unwrap();
|
||||
|
||||
let ssh_key_str = ssh_key2.to_string().unwrap();
|
||||
assert_eq!(ssh_key_str, input);
|
||||
}
|
||||
}
|
||||
Vendored
+633
@@ -0,0 +1,633 @@
|
||||
//! Signature algorithms supported by picky
|
||||
|
||||
use crate::hash::HashAlgorithm;
|
||||
use crate::key::ec::{EcComponent, EcCurve, NamedEcCurve};
|
||||
use crate::key::{KeyError, PrivateKey, PublicKey};
|
||||
|
||||
use picky_asn1_x509::{AlgorithmIdentifier, oids};
|
||||
use rsa::signature::Signer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum SignatureError {
|
||||
/// Key error
|
||||
#[error("Key error: {source}")]
|
||||
Key { source: KeyError },
|
||||
|
||||
/// RSA error
|
||||
#[error("RSA error: {context}")]
|
||||
Rsa { context: String },
|
||||
|
||||
/// EC error
|
||||
#[error("EC error: {context}")]
|
||||
Ec { context: String },
|
||||
|
||||
/// ED error
|
||||
#[error("ED error: {context}")]
|
||||
Ed { context: String },
|
||||
|
||||
/// invalid signature
|
||||
#[error("invalid signature")]
|
||||
BadSignature,
|
||||
|
||||
/// unsupported algorithm
|
||||
#[error("unsupported algorithm: {algorithm}")]
|
||||
UnsupportedAlgorithm { algorithm: String },
|
||||
}
|
||||
|
||||
impl From<rsa::errors::Error> for SignatureError {
|
||||
fn from(e: rsa::errors::Error) -> Self {
|
||||
SignatureError::Rsa { context: e.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rsa::signature::Error> for SignatureError {
|
||||
fn from(e: rsa::signature::Error) -> Self {
|
||||
SignatureError::Rsa { context: e.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeyError> for SignatureError {
|
||||
fn from(e: KeyError) -> Self {
|
||||
SignatureError::Key { source: e }
|
||||
}
|
||||
}
|
||||
|
||||
/// Supported signature algorithms
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum SignatureAlgorithm {
|
||||
RsaPkcs1v15(HashAlgorithm),
|
||||
Ecdsa(HashAlgorithm),
|
||||
Ed25519,
|
||||
}
|
||||
|
||||
impl TryFrom<&'_ AlgorithmIdentifier> for SignatureAlgorithm {
|
||||
type Error = SignatureError;
|
||||
|
||||
fn try_from(v: &AlgorithmIdentifier) -> Result<Self, Self::Error> {
|
||||
let oid_string: String = v.oid().into();
|
||||
match oid_string.as_str() {
|
||||
oids::MD5_WITH_RSA_ENCRYPTHION => Ok(Self::RsaPkcs1v15(HashAlgorithm::MD5)),
|
||||
oids::SHA1_WITH_RSA_ENCRYPTION => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA1)),
|
||||
oids::SHA224_WITH_RSA_ENCRYPTION => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA2_224)),
|
||||
oids::SHA256_WITH_RSA_ENCRYPTION => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA2_256)),
|
||||
oids::SHA384_WITH_RSA_ENCRYPTION => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA2_384)),
|
||||
oids::SHA512_WITH_RSA_ENCRYPTION => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA2_512)),
|
||||
oids::ID_RSASSA_PKCS1_V1_5_WITH_SHA3_384 => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA3_384)),
|
||||
oids::ID_RSASSA_PKCS1_V1_5_WITH_SHA3_512 => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA3_512)),
|
||||
oids::ECDSA_WITH_SHA256 => Ok(Self::Ecdsa(HashAlgorithm::SHA2_256)),
|
||||
oids::ECDSA_WITH_SHA384 => Ok(Self::Ecdsa(HashAlgorithm::SHA2_384)),
|
||||
oids::ED25519 => Ok(Self::Ed25519),
|
||||
_ => Err(SignatureError::UnsupportedAlgorithm { algorithm: oid_string }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<SignatureAlgorithm> for AlgorithmIdentifier {
|
||||
type Error = SignatureError;
|
||||
|
||||
fn try_from(ty: SignatureAlgorithm) -> Result<Self, Self::Error> {
|
||||
match ty {
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::MD5) => {
|
||||
Ok(AlgorithmIdentifier::new_md5_with_rsa_encryption())
|
||||
}
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1) => {
|
||||
Ok(AlgorithmIdentifier::new_sha1_with_rsa_encryption())
|
||||
}
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224) => {
|
||||
Ok(AlgorithmIdentifier::new_sha224_with_rsa_encryption())
|
||||
}
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256) => {
|
||||
Ok(AlgorithmIdentifier::new_sha256_with_rsa_encryption())
|
||||
}
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384) => {
|
||||
Ok(AlgorithmIdentifier::new_sha384_with_rsa_encryption())
|
||||
}
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512) => {
|
||||
Ok(AlgorithmIdentifier::new_sha512_with_rsa_encryption())
|
||||
}
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_384) => {
|
||||
Ok(AlgorithmIdentifier::new_sha3_384_with_rsa_encryption())
|
||||
}
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_512) => {
|
||||
Ok(AlgorithmIdentifier::new_sha3_512_with_rsa_encryption())
|
||||
}
|
||||
SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256) => Ok(AlgorithmIdentifier::new_ecdsa_with_sha256()),
|
||||
SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384) => Ok(AlgorithmIdentifier::new_ecdsa_with_sha384()),
|
||||
SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_512) => Ok(AlgorithmIdentifier::new_ecdsa_with_sha512()),
|
||||
SignatureAlgorithm::Ecdsa(hash) => {
|
||||
let msg = format!("ECDSA doesn't support {hash:?} hashing algorithm");
|
||||
Err(SignatureError::Ec { context: msg })
|
||||
}
|
||||
SignatureAlgorithm::Ed25519 => Ok(AlgorithmIdentifier::new_ed25519()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SignatureAlgorithm {
|
||||
pub fn from_algorithm_identifier(algorithm_identifier: &AlgorithmIdentifier) -> Result<Self, SignatureError> {
|
||||
Self::try_from(algorithm_identifier)
|
||||
}
|
||||
|
||||
pub fn sign(self, msg: &[u8], private_key: &PrivateKey) -> Result<Vec<u8>, SignatureError> {
|
||||
match self {
|
||||
SignatureAlgorithm::RsaPkcs1v15(picky_hash_algo) => {
|
||||
use rsa::signature::SignatureEncoding as _;
|
||||
use rsa::{RsaPrivateKey, pkcs1v15};
|
||||
|
||||
let rsa_private_key = RsaPrivateKey::try_from(private_key)?;
|
||||
|
||||
let signature = match picky_hash_algo {
|
||||
HashAlgorithm::MD5 => pkcs1v15::SigningKey::<md5::Md5>::new(rsa_private_key).try_sign(msg)?,
|
||||
HashAlgorithm::SHA1 => pkcs1v15::SigningKey::<sha1::Sha1>::new(rsa_private_key).try_sign(msg)?,
|
||||
HashAlgorithm::SHA2_224 => {
|
||||
pkcs1v15::SigningKey::<sha2::Sha224>::new(rsa_private_key).try_sign(msg)?
|
||||
}
|
||||
HashAlgorithm::SHA2_256 => {
|
||||
pkcs1v15::SigningKey::<sha2::Sha256>::new(rsa_private_key).try_sign(msg)?
|
||||
}
|
||||
HashAlgorithm::SHA2_384 => {
|
||||
pkcs1v15::SigningKey::<sha2::Sha384>::new(rsa_private_key).try_sign(msg)?
|
||||
}
|
||||
HashAlgorithm::SHA2_512 => {
|
||||
pkcs1v15::SigningKey::<sha2::Sha512>::new(rsa_private_key).try_sign(msg)?
|
||||
}
|
||||
HashAlgorithm::SHA3_384 => {
|
||||
pkcs1v15::SigningKey::<sha3::Sha3_384>::new(rsa_private_key).try_sign(msg)?
|
||||
}
|
||||
HashAlgorithm::SHA3_512 => {
|
||||
pkcs1v15::SigningKey::<sha3::Sha3_512>::new(rsa_private_key).try_sign(msg)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(signature.to_vec())
|
||||
}
|
||||
SignatureAlgorithm::Ecdsa(picky_hash_algo) => {
|
||||
use crate::key::ec::EcdsaKeypair;
|
||||
use p256::ecdsa::signature::Signer;
|
||||
|
||||
let ec_keypair = EcdsaKeypair::try_from(private_key)?;
|
||||
|
||||
match ec_keypair.curve() {
|
||||
NamedEcCurve::Known(EcCurve::NistP256) => match picky_hash_algo {
|
||||
HashAlgorithm::SHA2_256 => {
|
||||
let secret_validated =
|
||||
EcCurve::NistP256.validate_component(EcComponent::Secret(ec_keypair.secret()))?;
|
||||
|
||||
let key_bytes =
|
||||
p256::elliptic_curve::array::Array::try_from(secret_validated).map_err(|_| {
|
||||
SignatureError::Ec {
|
||||
context: format!(
|
||||
"validated secret is not the right size(expected: {}, actual: {})",
|
||||
EcCurve::NistP256.field_bytes_size(),
|
||||
secret_validated.len(),
|
||||
),
|
||||
}
|
||||
})?;
|
||||
let key =
|
||||
p256::ecdsa::SigningKey::from_bytes(&key_bytes).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Cannot decode p256 EC keypair: {e}"),
|
||||
})?;
|
||||
let sig: p256::ecdsa::Signature = key.try_sign(msg).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Cannot produce p256 signature: {e}"),
|
||||
})?;
|
||||
|
||||
Ok(sig.to_der().as_bytes().to_vec())
|
||||
}
|
||||
_ => Err(SignatureError::UnsupportedAlgorithm {
|
||||
algorithm: format!(
|
||||
"ECDSA P-256 curve with {picky_hash_algo:?} hash algorithm is not supported"
|
||||
),
|
||||
}),
|
||||
},
|
||||
NamedEcCurve::Known(EcCurve::NistP384) => match picky_hash_algo {
|
||||
HashAlgorithm::SHA2_384 => {
|
||||
let secret_validated =
|
||||
EcCurve::NistP384.validate_component(EcComponent::Secret(ec_keypair.secret()))?;
|
||||
|
||||
let key_bytes =
|
||||
p384::elliptic_curve::array::Array::try_from(secret_validated).map_err(|_| {
|
||||
SignatureError::Ec {
|
||||
context: format!(
|
||||
"validated secret is not the right size(expected: {}, actual: {})",
|
||||
EcCurve::NistP384.field_bytes_size(),
|
||||
secret_validated.len(),
|
||||
),
|
||||
}
|
||||
})?;
|
||||
|
||||
let key =
|
||||
p384::ecdsa::SigningKey::from_bytes(&key_bytes).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Cannot decode p384 EC keypair: {e}"),
|
||||
})?;
|
||||
let sig: p384::ecdsa::Signature = key.try_sign(msg).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Cannot produce p384 signature: {e}"),
|
||||
})?;
|
||||
Ok(sig.to_der().as_bytes().to_vec())
|
||||
}
|
||||
_ => Err(SignatureError::UnsupportedAlgorithm {
|
||||
algorithm: format!(
|
||||
"ECDSA P-384 curve with {picky_hash_algo:?} hash algorithm is not supported"
|
||||
),
|
||||
}),
|
||||
},
|
||||
NamedEcCurve::Known(EcCurve::NistP521) => match picky_hash_algo {
|
||||
HashAlgorithm::SHA2_512 => {
|
||||
let secret_validated =
|
||||
EcCurve::NistP521.validate_component(EcComponent::Secret(ec_keypair.secret()))?;
|
||||
|
||||
let key_bytes =
|
||||
p521::elliptic_curve::array::Array::try_from(secret_validated).map_err(|_| {
|
||||
SignatureError::Ec {
|
||||
context: format!(
|
||||
"validated secret is not the right size(expected: {}, actual: {})",
|
||||
EcCurve::NistP521.field_bytes_size(),
|
||||
secret_validated.len(),
|
||||
),
|
||||
}
|
||||
})?;
|
||||
|
||||
let key =
|
||||
p521::ecdsa::SigningKey::from_bytes(&key_bytes).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Cannot decode p521 EC keypair: {e}"),
|
||||
})?;
|
||||
let sig: p521::ecdsa::Signature = key.try_sign(msg).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Cannot produce p521 signature: {e}"),
|
||||
})?;
|
||||
Ok(sig.to_der().as_bytes().to_vec())
|
||||
}
|
||||
_ => Err(SignatureError::UnsupportedAlgorithm {
|
||||
algorithm: format!(
|
||||
"ECDSA P-521 curve with {picky_hash_algo:?} hash algorithm is not supported"
|
||||
),
|
||||
}),
|
||||
},
|
||||
NamedEcCurve::Unsupported(oid) => Err(KeyError::unsupported_curve(oid, "signing").into()),
|
||||
}
|
||||
}
|
||||
SignatureAlgorithm::Ed25519 => {
|
||||
use crate::key::ed::{EdAlgorithm, EdKeypair, EdPublicKey, NamedEdAlgorithm};
|
||||
|
||||
let keypair = EdKeypair::try_from(private_key)?;
|
||||
|
||||
let public_key = EdPublicKey::try_from(&keypair)?;
|
||||
|
||||
match keypair.algorithm() {
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::Ed25519) => {
|
||||
let public_key = public_key.data().try_into().map_err(|e| SignatureError::Ed {
|
||||
context: format!("invalid key size: {e}"),
|
||||
})?;
|
||||
let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(public_key).map_err(
|
||||
|e: ed25519_dalek::ed25519::Error| SignatureError::Ed {
|
||||
context: format!("Cannot decode ed25519 public key: {e}"),
|
||||
},
|
||||
)?;
|
||||
|
||||
let secret_key: ed25519_dalek::SecretKey =
|
||||
keypair.secret().try_into().map_err(|e| SignatureError::Ed {
|
||||
context: format!("invalid secret key size: {e}"),
|
||||
})?;
|
||||
|
||||
let esk = ed25519_dalek::hazmat::ExpandedSecretKey::from(&secret_key);
|
||||
|
||||
let signature = ed25519_dalek::hazmat::raw_sign::<sha2::Sha512>(&esk, msg, &verifying_key);
|
||||
|
||||
Ok(signature.to_vec())
|
||||
}
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::X25519) => Err(SignatureError::Ed {
|
||||
context: "X25519 algorithm is not designed for signing".to_string(),
|
||||
}),
|
||||
NamedEdAlgorithm::Unsupported(oid) => {
|
||||
Err(KeyError::unsupported_ed_algorithm(oid, "signing").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify(self, public_key: &PublicKey, msg: &[u8], signature: &[u8]) -> Result<(), SignatureError> {
|
||||
match self {
|
||||
SignatureAlgorithm::RsaPkcs1v15(picky_hash_algo) => {
|
||||
use rsa::signature::Verifier as _;
|
||||
use rsa::{RsaPublicKey, pkcs1v15};
|
||||
|
||||
let rsa_public_key = RsaPublicKey::try_from(public_key)?;
|
||||
let signature = pkcs1v15::Signature::try_from(signature)?;
|
||||
|
||||
match picky_hash_algo {
|
||||
HashAlgorithm::MD5 => {
|
||||
pkcs1v15::VerifyingKey::<md5::Md5>::new(rsa_public_key).verify(msg, &signature)
|
||||
}
|
||||
HashAlgorithm::SHA1 => {
|
||||
pkcs1v15::VerifyingKey::<sha1::Sha1>::new(rsa_public_key).verify(msg, &signature)
|
||||
}
|
||||
HashAlgorithm::SHA2_224 => {
|
||||
pkcs1v15::VerifyingKey::<sha2::Sha224>::new(rsa_public_key).verify(msg, &signature)
|
||||
}
|
||||
HashAlgorithm::SHA2_256 => {
|
||||
pkcs1v15::VerifyingKey::<sha2::Sha256>::new(rsa_public_key).verify(msg, &signature)
|
||||
}
|
||||
HashAlgorithm::SHA2_384 => {
|
||||
pkcs1v15::VerifyingKey::<sha2::Sha384>::new(rsa_public_key).verify(msg, &signature)
|
||||
}
|
||||
HashAlgorithm::SHA2_512 => {
|
||||
pkcs1v15::VerifyingKey::<sha2::Sha512>::new(rsa_public_key).verify(msg, &signature)
|
||||
}
|
||||
HashAlgorithm::SHA3_384 => {
|
||||
pkcs1v15::VerifyingKey::<sha3::Sha3_384>::new(rsa_public_key).verify(msg, &signature)
|
||||
}
|
||||
HashAlgorithm::SHA3_512 => {
|
||||
pkcs1v15::VerifyingKey::<sha3::Sha3_512>::new(rsa_public_key).verify(msg, &signature)
|
||||
}
|
||||
}
|
||||
.map_err(|_| SignatureError::BadSignature)?;
|
||||
}
|
||||
SignatureAlgorithm::Ecdsa(picky_hash_algo) => {
|
||||
let ec_pub_key = crate::key::ec::EcdsaPublicKey::try_from(public_key)?;
|
||||
|
||||
let curve = match ec_pub_key.curve() {
|
||||
NamedEcCurve::Known(curve) => curve,
|
||||
NamedEcCurve::Unsupported(oid) => return Err(KeyError::unsupported_curve(oid, "verifying").into()),
|
||||
};
|
||||
|
||||
match picky_hash_algo {
|
||||
HashAlgorithm::SHA2_256 => {
|
||||
use p256::ecdsa::signature::Verifier;
|
||||
|
||||
match curve {
|
||||
EcCurve::NistP256 => {}
|
||||
curve => {
|
||||
return Err(SignatureError::UnsupportedAlgorithm {
|
||||
algorithm: format!("SHA256 hash algorithm can't be used with `{curve}` curve"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let encoded_point = p256::Sec1Point::from_bytes(ec_pub_key.encoded_point()).map_err(|e| {
|
||||
SignatureError::Ec {
|
||||
context: format!("Cannot parse p256 public key from der bytes: {e}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
let vkey = p256::ecdsa::VerifyingKey::from_sec1_point(&encoded_point).map_err(|e| {
|
||||
SignatureError::Ec {
|
||||
context: format!("Cannot parse p256 encoded point: {e}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
let signature =
|
||||
p256::ecdsa::Signature::from_der(signature).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Cannot parse p256 signature: {e}"),
|
||||
})?;
|
||||
vkey.verify(msg, &signature).map_err(|_| SignatureError::BadSignature)?
|
||||
}
|
||||
HashAlgorithm::SHA2_384 => {
|
||||
use p384::ecdsa::signature::Verifier;
|
||||
|
||||
match curve {
|
||||
EcCurve::NistP384 => {}
|
||||
curve => {
|
||||
return Err(SignatureError::UnsupportedAlgorithm {
|
||||
algorithm: format!("SHA384 hash algorithm can't be used with `{curve}` curve"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let encoded_point = p384::Sec1Point::from_bytes(ec_pub_key.encoded_point()).map_err(|e| {
|
||||
SignatureError::Ec {
|
||||
context: format!("Cannot parse p384 public key from der bytes: {e}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
let vkey = p384::ecdsa::VerifyingKey::from_sec1_point(&encoded_point).map_err(|e| {
|
||||
SignatureError::Ec {
|
||||
context: format!("Cannot parse p384 encoded point: {e}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
let signature =
|
||||
p384::ecdsa::Signature::from_der(signature).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Cannot parse p384 signature: {e}"),
|
||||
})?;
|
||||
vkey.verify(msg, &signature).map_err(|_| SignatureError::BadSignature)?
|
||||
}
|
||||
HashAlgorithm::SHA2_512 => {
|
||||
use p521::ecdsa::signature::Verifier;
|
||||
|
||||
match curve {
|
||||
EcCurve::NistP521 => {}
|
||||
curve => {
|
||||
return Err(SignatureError::UnsupportedAlgorithm {
|
||||
algorithm: format!("SHA512 hash algorithm can't be used with `{curve}` curve"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let encoded_point = p521::Sec1Point::from_bytes(ec_pub_key.encoded_point()).map_err(|e| {
|
||||
SignatureError::Ec {
|
||||
context: format!("Cannot parse p521 public key from der bytes: {e}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
let vkey = p521::ecdsa::VerifyingKey::from_sec1_point(&encoded_point).map_err(|e| {
|
||||
SignatureError::Ec {
|
||||
context: format!("Cannot parse p521 encoded point: {e}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
let signature =
|
||||
p521::ecdsa::Signature::from_der(signature).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Cannot parse p521 signature: {e}"),
|
||||
})?;
|
||||
vkey.verify(msg, &signature).map_err(|_| SignatureError::BadSignature)?
|
||||
}
|
||||
_ => {
|
||||
return Err(SignatureError::UnsupportedAlgorithm {
|
||||
algorithm: format!("ECDSA with {picky_hash_algo:?} hash algorithm is not supported"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
SignatureAlgorithm::Ed25519 => {
|
||||
use crate::key::ed::{EdAlgorithm, EdPublicKey, NamedEdAlgorithm};
|
||||
use ed25519_dalek::Verifier;
|
||||
|
||||
let public_key = EdPublicKey::try_from(public_key)?;
|
||||
|
||||
match public_key.algorithm() {
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::Ed25519) => {
|
||||
let public_key = public_key.data().try_into().map_err(|e| SignatureError::Ed {
|
||||
context: format!("invalid key size: {e}"),
|
||||
})?;
|
||||
let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(public_key).map_err(
|
||||
|e: ed25519_dalek::ed25519::Error| SignatureError::Ed {
|
||||
context: format!("Cannot decode ed25519 public key: {e}"),
|
||||
},
|
||||
)?;
|
||||
|
||||
let signature = signature.try_into().map_err(|e| SignatureError::Ed {
|
||||
context: format!("invalid signature size: {e}"),
|
||||
})?;
|
||||
let signature = ed25519_dalek::Signature::from_bytes(signature);
|
||||
|
||||
verifying_key
|
||||
.verify(msg, &signature)
|
||||
.map_err(|_| SignatureError::BadSignature)?;
|
||||
}
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::X25519) => {
|
||||
return Err(SignatureError::Ed {
|
||||
context: "X25519 algorithm is not designed for signing".to_string(),
|
||||
});
|
||||
}
|
||||
NamedEdAlgorithm::Unsupported(oid) => {
|
||||
return Err(KeyError::unsupported_ed_algorithm(oid, "verifying").into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hash_algorithm(&self) -> HashAlgorithm {
|
||||
match &self {
|
||||
SignatureAlgorithm::RsaPkcs1v15(hash_algo) => *hash_algo,
|
||||
SignatureAlgorithm::Ecdsa(hash_algo) => *hash_algo,
|
||||
SignatureAlgorithm::Ed25519 => HashAlgorithm::SHA2_512,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod ec_tests {
|
||||
use super::*;
|
||||
use rstest::*;
|
||||
|
||||
const EC_PRIVATE_KEY_NIST256_PEM: &str = r#"-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEICHio5XUa+RbeFfGtGHfbPWehTFJJtCB4/izKHJ9Vm+goAoGCCqGSM49
|
||||
AwEHoUQDQgAEh7ZqcI6f0tgqq7nqdcxWM6P4GGCfkWc4q11uXFjtXOKHKCV3LzMY
|
||||
g8/V1PD/YOh0HodRJAjkjXub8AmYxiTcXw==
|
||||
-----END EC PRIVATE KEY-----"#;
|
||||
|
||||
const EC_PRIVATE_KEY_NIST384_PEM: &str = r#"-----BEGIN EC PRIVATE KEY-----
|
||||
MIGkAgEBBDDT8VOfdzHbIRaWOO1F0vgotY2qM2FfYS3zpdKE7Vqbh26hFsUw+iaG
|
||||
GmGnT+29kg+gBwYFK4EEACKhZANiAAQFvVVUKRdN3/bqaEpDA1aHu8FEd3ujuyS0
|
||||
AadG6QAiZxH37BGumBcyTTeGHyArqb+GTpsHTUXASbP+P+p5JgkfF9wBMF1SVTvu
|
||||
ACZOYcqzGbsAXXdMYqewckhc42ye0u0=
|
||||
-----END EC PRIVATE KEY-----"#;
|
||||
|
||||
#[rstest]
|
||||
#[case(HashAlgorithm::MD5, false)]
|
||||
#[case(HashAlgorithm::SHA1, false)]
|
||||
#[case(HashAlgorithm::SHA2_224, false)]
|
||||
#[case(HashAlgorithm::SHA2_256, true)]
|
||||
#[case(HashAlgorithm::SHA2_384, true)]
|
||||
#[case(HashAlgorithm::SHA2_512, true)]
|
||||
#[case(HashAlgorithm::SHA3_384, false)]
|
||||
#[case(HashAlgorithm::SHA3_512, false)]
|
||||
fn algorithm_identifier_conversions(#[case] hash: HashAlgorithm, #[case] success: bool) {
|
||||
let signature_algorithm = SignatureAlgorithm::Ecdsa(hash);
|
||||
let algorithm_identifier = AlgorithmIdentifier::try_from(signature_algorithm);
|
||||
if success {
|
||||
assert!(algorithm_identifier.is_ok());
|
||||
} else {
|
||||
assert!(matches!(algorithm_identifier, Err(SignatureError::Ec { context: _ })));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_bad_signature() {
|
||||
let private_key_signature = PrivateKey::from_pem_str(EC_PRIVATE_KEY_NIST256_PEM).unwrap();
|
||||
let signature_algorithm = SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256);
|
||||
|
||||
let msg = b"hello world";
|
||||
let signature = signature_algorithm.sign(msg, &private_key_signature).unwrap();
|
||||
|
||||
let another_ec_private_key_nist256_pem = r#"-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIBVYtZ17YMj89Kuu47TOxJlLVlk7MDUuAlFrVXxexgkSoAoGCCqGSM49
|
||||
AwEHoUQDQgAE/irzdOJk28zjVv3sov15/NLIOxoIwL9kM2p/RfQAslATwHpD/T79
|
||||
csaQwO9jFvbQFIpCvcMRjaunLfhIWiYDdg==
|
||||
-----END EC PRIVATE KEY-----"#;
|
||||
|
||||
let another_private_key = PrivateKey::from_pem_str(another_ec_private_key_nist256_pem).unwrap();
|
||||
let wrong_public_key = PublicKey::try_from(another_private_key).unwrap();
|
||||
assert!(matches!(
|
||||
signature_algorithm.verify(&wrong_public_key, msg, &signature),
|
||||
Err(SignatureError::BadSignature)
|
||||
));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::EC_NIST256_PK_1, HashAlgorithm::SHA2_256, true)]
|
||||
#[case(picky_test_data::EC_NIST384_PK_1, HashAlgorithm::SHA2_384, true)]
|
||||
#[case(picky_test_data::EC_NIST521_PK_1, HashAlgorithm::SHA2_512, true)]
|
||||
fn sign_and_verify(#[case] key_pem: &str, #[case] hash: HashAlgorithm, #[case] sign_successful: bool) {
|
||||
let private_key = PrivateKey::from_pem_str(key_pem).unwrap();
|
||||
|
||||
let signature_algorithm = SignatureAlgorithm::Ecdsa(hash);
|
||||
|
||||
let msg = b"hello world";
|
||||
let signature = signature_algorithm.sign(msg, &private_key);
|
||||
assert_eq!(signature.is_ok(), sign_successful);
|
||||
|
||||
if !sign_successful {
|
||||
return;
|
||||
}
|
||||
|
||||
let public_key = PublicKey::try_from(private_key).unwrap();
|
||||
signature_algorithm
|
||||
.verify(&public_key, msg, &signature.unwrap())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(EC_PRIVATE_KEY_NIST256_PEM, &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, HashAlgorithm::SHA2_256)]
|
||||
#[case(EC_PRIVATE_KEY_NIST384_PEM, &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING, HashAlgorithm::SHA2_384)]
|
||||
fn sign_and_verify_compatibility_with_ring(
|
||||
#[case] key_pem: &str,
|
||||
#[case] algorithm: &'static ring::signature::EcdsaSigningAlgorithm,
|
||||
#[case] hash: HashAlgorithm,
|
||||
) {
|
||||
// sign using ring
|
||||
let private_key = PrivateKey::from_pem_str(key_pem).unwrap();
|
||||
let public_key = PublicKey::try_from(private_key).unwrap();
|
||||
let msg = b"hello world";
|
||||
|
||||
let (privk, pubk) = match hash {
|
||||
HashAlgorithm::SHA2_256 => {
|
||||
use p256::Sec1Point;
|
||||
let k = p256::SecretKey::from_sec1_pem(key_pem).unwrap();
|
||||
(
|
||||
k.to_bytes().as_slice().to_vec(),
|
||||
Into::<Sec1Point>::into(k.public_key()).as_bytes().to_vec(),
|
||||
)
|
||||
}
|
||||
HashAlgorithm::SHA2_384 => {
|
||||
use p384::Sec1Point;
|
||||
let k = p384::SecretKey::from_sec1_pem(key_pem).unwrap();
|
||||
(
|
||||
k.to_bytes().as_slice().to_vec(),
|
||||
Into::<Sec1Point>::into(k.public_key()).as_bytes().to_vec(),
|
||||
)
|
||||
}
|
||||
_ => panic!("no this condition"),
|
||||
};
|
||||
|
||||
let rng = ring::rand::SystemRandom::new();
|
||||
|
||||
let keypair =
|
||||
ring::signature::EcdsaKeyPair::from_private_key_and_public_key(algorithm, &privk, &pubk, &rng).unwrap();
|
||||
|
||||
let rng = ring::rand::SystemRandom::new();
|
||||
let signature = keypair.sign(&rng, msg).unwrap();
|
||||
let sig = signature.as_ref().to_vec();
|
||||
|
||||
// verify using rust-crypto
|
||||
let signature_algorithm = SignatureAlgorithm::Ecdsa(hash);
|
||||
|
||||
signature_algorithm.verify(&public_key, msg, &sig).unwrap();
|
||||
}
|
||||
}
|
||||
+1070
File diff suppressed because it is too large
Load Diff
Vendored
+595
@@ -0,0 +1,595 @@
|
||||
use crate::key::ec::{EcCurve, NamedEcCurve};
|
||||
use crate::key::ed::NamedEdAlgorithm;
|
||||
use crate::key::{EdAlgorithm, PrivateKey, PublicKey};
|
||||
use crate::ssh::certificate::{
|
||||
SshCertKeyType, SshCertType, SshCertTypeError, SshCertificate, SshCertificateError, SshCriticalOption,
|
||||
SshCriticalOptionError, SshCriticalOptionType, SshExtension, SshExtensionError, SshExtensionType, SshSignature,
|
||||
SshSignatureError, SshSignatureFormat, Timestamp,
|
||||
};
|
||||
use crate::ssh::private_key::{KdfOption, SshBasePrivateKey, SshPrivateKeyError};
|
||||
use crate::ssh::public_key::{SshBasePublicKey, SshPublicKey, SshPublicKeyError};
|
||||
use crate::ssh::{Base64Reader, SSH_COMBO_ED25519_KEY_LENGTH, key_type, read_until_linebreak, read_until_whitespace};
|
||||
|
||||
use super::certificate::SshSignatureBlob;
|
||||
use base64::engine::general_purpose;
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use crypto_bigint::BoxedUint;
|
||||
use picky_asn1_x509::oid::ObjectIdentifier;
|
||||
use picky_asn1_x509::oids;
|
||||
use std::io::{self, Cursor, Read};
|
||||
|
||||
pub trait SshReadExt {
|
||||
type Error;
|
||||
|
||||
fn read_ssh_string(&mut self) -> Result<String, Self::Error>;
|
||||
fn read_ssh_bytes(&mut self) -> Result<Vec<u8>, Self::Error>;
|
||||
fn read_ssh_mpint(&mut self) -> Result<BoxedUint, Self::Error>;
|
||||
}
|
||||
|
||||
impl<T> SshReadExt for T
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
type Error = io::Error;
|
||||
|
||||
fn read_ssh_string(&mut self) -> Result<String, Self::Error> {
|
||||
let size = self.read_u32::<BigEndian>()? as usize;
|
||||
let mut buffer = vec![0; size];
|
||||
self.read_exact(&mut buffer)?;
|
||||
|
||||
Ok(String::from_utf8_lossy(&buffer).into_owned())
|
||||
}
|
||||
|
||||
fn read_ssh_bytes(&mut self) -> Result<Vec<u8>, Self::Error> {
|
||||
let size = self.read_u32::<BigEndian>()? as usize;
|
||||
let mut buffer = vec![0; size];
|
||||
self.read_exact(&mut buffer)?;
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
fn read_ssh_mpint(&mut self) -> Result<BoxedUint, Self::Error> {
|
||||
let size = self.read_u32::<BigEndian>()? as usize;
|
||||
let mut buffer = vec![0; size];
|
||||
self.read_exact(&mut buffer)?;
|
||||
|
||||
if buffer[0] == 0 {
|
||||
buffer.remove(0);
|
||||
}
|
||||
|
||||
Ok(BoxedUint::from_be_slice_vartime(&buffer))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SshComplexTypeDecode: Sized {
|
||||
type Error;
|
||||
|
||||
fn decode(stream: impl Read) -> Result<Self, Self::Error>;
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for SshCertType {
|
||||
type Error = SshCertTypeError;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
SshCertType::try_from(stream.read_u32::<BigEndian>()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for SshCriticalOption {
|
||||
type Error = SshCriticalOptionError;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let option_type: String = stream.read_ssh_string()?;
|
||||
let data: String = stream.read_ssh_string()?;
|
||||
Ok(SshCriticalOption {
|
||||
option_type: SshCriticalOptionType::try_from(option_type)?,
|
||||
data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SshComplexTypeDecode for Vec<T>
|
||||
where
|
||||
T: SshComplexTypeDecode,
|
||||
T::Error: From<std::io::Error>,
|
||||
{
|
||||
type Error = T::Error;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let data = stream.read_ssh_bytes()?;
|
||||
let len = data.len() as u64;
|
||||
let mut cursor = Cursor::new(data);
|
||||
let mut res = Vec::new();
|
||||
while cursor.position() < len {
|
||||
let elem: Result<T, Self::Error> = SshComplexTypeDecode::decode(&mut cursor);
|
||||
res.push(elem?);
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for SshExtension {
|
||||
type Error = SshExtensionError;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let extension_type = stream.read_ssh_string()?;
|
||||
let data = stream.read_ssh_string()?;
|
||||
Ok(SshExtension {
|
||||
extension_type: SshExtensionType::try_from(extension_type)?,
|
||||
data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for Vec<String> {
|
||||
type Error = io::Error;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let data = stream.read_ssh_bytes()?;
|
||||
let len = data.len();
|
||||
let mut cursor = Cursor::new(data);
|
||||
let mut res = Vec::new();
|
||||
while cursor.position() < len as u64 {
|
||||
res.push(cursor.read_ssh_string()?);
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for SshSignature {
|
||||
type Error = SshSignatureError;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let _overall_size = stream.read_u32::<BigEndian>()?;
|
||||
|
||||
let format = SshSignatureFormat::new(stream.read_ssh_string()?.as_str())?;
|
||||
let data = stream.read_ssh_bytes()?;
|
||||
|
||||
match format {
|
||||
SshSignatureFormat::SkEd25519 | SshSignatureFormat::SkEcdsaSha2NistP256 => {
|
||||
let flags = stream.read_u8()?;
|
||||
let counter = stream.read_u32::<BigEndian>()?;
|
||||
|
||||
Ok(SshSignature {
|
||||
format,
|
||||
blob: SshSignatureBlob::Sk { data, flags, counter },
|
||||
})
|
||||
}
|
||||
_ => Ok(SshSignature {
|
||||
format,
|
||||
blob: SshSignatureBlob::Standard(data),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for KdfOption {
|
||||
type Error = io::Error;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let data = stream.read_ssh_bytes()?;
|
||||
if data.is_empty() {
|
||||
return Ok(KdfOption::default());
|
||||
}
|
||||
let mut data = data.as_slice();
|
||||
let salt = data.read_ssh_bytes()?;
|
||||
let rounds = data.read_u32::<BigEndian>()?;
|
||||
Ok(KdfOption { salt, rounds })
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for Timestamp {
|
||||
type Error = io::Error;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let timestamp = stream.read_u64::<BigEndian>()?;
|
||||
let time = Timestamp::from(timestamp);
|
||||
Ok(time)
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for SshBasePublicKey {
|
||||
type Error = SshPublicKeyError;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let key_type = stream.read_ssh_string()?;
|
||||
match key_type.as_str() {
|
||||
key_type::RSA => {
|
||||
let e = stream.read_ssh_mpint()?;
|
||||
let n = stream.read_ssh_mpint()?;
|
||||
Ok(SshBasePublicKey::Rsa(PublicKey::from_rsa_components(&n, &e)))
|
||||
}
|
||||
key_type::ECDSA_SHA2_NIST_P256 | key_type::ECDSA_SHA2_NIST_P384 | key_type::ECDSA_SHA2_NIST_P521 => {
|
||||
let (curve, point) = decode_ec_public_key_body_impl(key_type.as_str(), &mut stream)?;
|
||||
Ok(SshBasePublicKey::Ec(PublicKey::from_ec_encoded_components(
|
||||
&curve.into(),
|
||||
&point,
|
||||
)))
|
||||
}
|
||||
key_type::ED25519 => {
|
||||
let (algorithm, public_key) = decode_ed25519_public_key_body_impl(key_type.as_str(), &mut stream)?;
|
||||
|
||||
Ok(SshBasePublicKey::Ed(PublicKey::from_ed_encoded_components(
|
||||
&algorithm.into(),
|
||||
&public_key,
|
||||
)))
|
||||
}
|
||||
key_type::SK_ECDSA_SHA2_NIST_P256 => {
|
||||
let (curve, point) = decode_ec_public_key_body_impl(key_type.as_str(), &mut stream)?;
|
||||
let base_key = PublicKey::from_ec_encoded_components(&curve.into(), &point);
|
||||
let application = stream.read_ssh_string()?;
|
||||
|
||||
Ok(SshBasePublicKey::SkEcdsaSha2NistP256 { base_key, application })
|
||||
}
|
||||
key_type::SK_ED25519 => {
|
||||
let (algorithm, public_key) = decode_ed25519_public_key_body_impl(key_type.as_str(), &mut stream)?;
|
||||
let base_key = PublicKey::from_ed_encoded_components(&algorithm.into(), &public_key);
|
||||
let application = stream.read_ssh_string()?;
|
||||
|
||||
Ok(SshBasePublicKey::SkEd25519 { base_key, application })
|
||||
}
|
||||
_ => Err(SshPublicKeyError::UnknownKeyType),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_ed25519_public_key_body_impl(
|
||||
key_type: &str,
|
||||
stream: &mut impl Read,
|
||||
) -> Result<(NamedEdAlgorithm, Vec<u8>), SshPublicKeyError> {
|
||||
let algorithm = match key_type {
|
||||
key_type::ED25519 => NamedEdAlgorithm::Known(EdAlgorithm::Ed25519),
|
||||
key_type::SK_ED25519 => NamedEdAlgorithm::Known(EdAlgorithm::Ed25519),
|
||||
_ => {
|
||||
return Err(SshPublicKeyError::UnknownKeyType);
|
||||
}
|
||||
};
|
||||
let public_key = stream.read_ssh_bytes()?;
|
||||
Ok((algorithm, public_key))
|
||||
}
|
||||
|
||||
fn decode_ec_public_key_body_impl(
|
||||
key_type: &str,
|
||||
stream: &mut impl Read,
|
||||
) -> Result<(NamedEcCurve, Vec<u8>), SshPublicKeyError> {
|
||||
let curve = match key_type {
|
||||
key_type::ECDSA_SHA2_NIST_P256 => NamedEcCurve::Known(EcCurve::NistP256),
|
||||
key_type::ECDSA_SHA2_NIST_P384 => NamedEcCurve::Known(EcCurve::NistP384),
|
||||
key_type::ECDSA_SHA2_NIST_P521 => NamedEcCurve::Unsupported(oids::secp521r1()),
|
||||
key_type::SK_ECDSA_SHA2_NIST_P256 => NamedEcCurve::Known(EcCurve::NistP256),
|
||||
_ => {
|
||||
return Err(SshPublicKeyError::UnknownKeyType);
|
||||
}
|
||||
};
|
||||
|
||||
// Duplicated information about key type
|
||||
let _identifier = stream.read_ssh_string()?;
|
||||
|
||||
// Public key encoded from an elliptic curve point into an
|
||||
// octet string as per [RFC](https://datatracker.ietf.org/doc/html/rfc5656#section-3.1).
|
||||
let point_data = stream.read_ssh_bytes()?;
|
||||
|
||||
Ok((curve, point_data))
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for SshPublicKey {
|
||||
type Error = SshPublicKeyError;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let mut buffer = Vec::with_capacity(1024);
|
||||
|
||||
read_until_whitespace(&mut stream, &mut buffer)?;
|
||||
|
||||
let header = String::from_utf8_lossy(&buffer).into_owned();
|
||||
buffer.clear();
|
||||
|
||||
let inner_key = match header.as_str() {
|
||||
key_type::RSA
|
||||
| key_type::ECDSA_SHA2_NIST_P256
|
||||
| key_type::ECDSA_SHA2_NIST_P384
|
||||
| key_type::ECDSA_SHA2_NIST_P521
|
||||
| key_type::ED25519
|
||||
| key_type::SK_ECDSA_SHA2_NIST_P256
|
||||
| key_type::SK_ED25519 => {
|
||||
read_until_whitespace(&mut stream, &mut buffer)?;
|
||||
let mut slice = buffer.as_slice();
|
||||
let decoder = Base64Reader::new(&mut slice, &general_purpose::STANDARD);
|
||||
SshComplexTypeDecode::decode(decoder)?
|
||||
}
|
||||
_ => return Err(SshPublicKeyError::UnknownKeyType),
|
||||
};
|
||||
|
||||
buffer.clear();
|
||||
read_until_linebreak(&mut stream, &mut buffer)?;
|
||||
let comment = core::str::from_utf8(&buffer)?.trim_end().to_owned();
|
||||
|
||||
Ok(SshPublicKey { inner_key, comment })
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for SshBasePrivateKey {
|
||||
type Error = SshPrivateKeyError;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let key_type = stream.read_ssh_string()?;
|
||||
match key_type.as_str() {
|
||||
key_type::RSA => {
|
||||
let n_constant = stream.read_ssh_mpint()?;
|
||||
let e_constant = stream.read_ssh_mpint()?;
|
||||
let d_constant = stream.read_ssh_mpint()?;
|
||||
let _iqmp = stream.read_ssh_mpint()?;
|
||||
let p_constant = stream.read_ssh_mpint()?;
|
||||
let q_constant = stream.read_ssh_mpint()?;
|
||||
|
||||
Ok(SshBasePrivateKey::Rsa(PrivateKey::from_rsa_components(
|
||||
&n_constant,
|
||||
&e_constant,
|
||||
&d_constant,
|
||||
&[p_constant, q_constant],
|
||||
)?))
|
||||
}
|
||||
key_type::ECDSA_SHA2_NIST_P256 | key_type::ECDSA_SHA2_NIST_P384 | key_type::ECDSA_SHA2_NIST_P521 => {
|
||||
let (curve, point) = decode_ec_public_key_body_impl(key_type.as_str(), &mut stream)?;
|
||||
|
||||
let private_key_secret = stream.read_ssh_mpint()?.to_be_bytes_trimmed_vartime();
|
||||
|
||||
Ok(SshBasePrivateKey::Ec(PrivateKey::from_ec_encoded_components(
|
||||
curve.into(),
|
||||
&private_key_secret,
|
||||
Some(point.as_slice()),
|
||||
)))
|
||||
}
|
||||
key_type::ED25519 => {
|
||||
let (algorithm, public_key) = decode_ed25519_public_key_body_impl(key_type.as_str(), &mut stream)?;
|
||||
|
||||
let private_key_secret = stream.read_ssh_mpint()?.to_be_bytes_trimmed_vartime();
|
||||
|
||||
// OpenSSH is really strange in regards to private ed25519 keys. It stores them as
|
||||
// 64 byte-array, but actually only first 32 bytes are the private key, and the rest
|
||||
// is public key copy
|
||||
if private_key_secret.len() != SSH_COMBO_ED25519_KEY_LENGTH {
|
||||
return Err(SshPrivateKeyError::InvalidKeyFormat);
|
||||
}
|
||||
|
||||
let private_key_secret = &private_key_secret[..ed25519_dalek::SECRET_KEY_LENGTH];
|
||||
|
||||
Ok(SshBasePrivateKey::Ed(PrivateKey::from_ed_encoded_components(
|
||||
algorithm.into(),
|
||||
private_key_secret,
|
||||
Some(&public_key),
|
||||
)))
|
||||
}
|
||||
key_type::SK_ECDSA_SHA2_NIST_P256 => {
|
||||
let (_curve, point) = decode_ec_public_key_body_impl(key_type.as_str(), &mut stream)?;
|
||||
|
||||
let application = stream.read_ssh_string()?;
|
||||
let flags = stream.read_u8()?;
|
||||
let handle = stream.read_ssh_bytes()?;
|
||||
let _reserved = stream.read_ssh_bytes()?;
|
||||
|
||||
Ok(SshBasePrivateKey::SkEcdsaSha2NistP256 {
|
||||
public_key: PublicKey::from_ec_encoded_components(
|
||||
&ObjectIdentifier::from(NamedEcCurve::Known(EcCurve::NistP256)),
|
||||
&point,
|
||||
),
|
||||
application,
|
||||
flags,
|
||||
handle,
|
||||
})
|
||||
}
|
||||
key_type::SK_ED25519 => {
|
||||
let (_algorithm, public_key) = decode_ed25519_public_key_body_impl(key_type.as_str(), &mut stream)?;
|
||||
|
||||
let application = stream.read_ssh_string()?;
|
||||
let flags = stream.read_u8()?;
|
||||
let handle = stream.read_ssh_bytes()?;
|
||||
let _reserved = stream.read_ssh_bytes()?;
|
||||
|
||||
Ok(SshBasePrivateKey::SkEd25519 {
|
||||
public_key: PublicKey::from_ed_encoded_components(
|
||||
&ObjectIdentifier::from(EdAlgorithm::Ed25519),
|
||||
&public_key,
|
||||
),
|
||||
application,
|
||||
flags,
|
||||
handle,
|
||||
})
|
||||
}
|
||||
key_type => Err(SshPrivateKeyError::UnsupportedKeyType(key_type.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for SshCertificate {
|
||||
type Error = SshCertificateError;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let mut cert_type = Vec::new();
|
||||
read_until_whitespace(&mut stream, &mut cert_type)?;
|
||||
|
||||
let _ = SshCertKeyType::try_from(String::from_utf8(cert_type)?)?;
|
||||
|
||||
let mut cert_data = Vec::new();
|
||||
read_until_whitespace(&mut stream, &mut cert_data)?;
|
||||
|
||||
let mut cert_data = cert_data.as_slice();
|
||||
let mut cert_data = Base64Reader::new(&mut cert_data, &general_purpose::STANDARD);
|
||||
|
||||
let cert_key_type = cert_data.read_ssh_string()?;
|
||||
let cert_key_type = SshCertKeyType::try_from(cert_key_type)?;
|
||||
|
||||
let nonce = cert_data.read_ssh_bytes()?;
|
||||
|
||||
let inner_public_key = match &cert_key_type {
|
||||
SshCertKeyType::SshRsaV01 | SshCertKeyType::RsaSha2_256V01 | SshCertKeyType::RsaSha2_512v01 => {
|
||||
let e = cert_data.read_ssh_mpint()?;
|
||||
let n = cert_data.read_ssh_mpint()?;
|
||||
SshBasePublicKey::Rsa(PublicKey::from_rsa_components(&n, &e))
|
||||
}
|
||||
SshCertKeyType::EcdsaSha2Nistp256V01
|
||||
| SshCertKeyType::EcdsaSha2Nistp384V01
|
||||
| SshCertKeyType::EcdsaSha2Nistp521V01 => {
|
||||
let curve = match cert_key_type {
|
||||
SshCertKeyType::EcdsaSha2Nistp256V01 => NamedEcCurve::Known(EcCurve::NistP256),
|
||||
SshCertKeyType::EcdsaSha2Nistp384V01 => NamedEcCurve::Known(EcCurve::NistP384),
|
||||
SshCertKeyType::EcdsaSha2Nistp521V01 => NamedEcCurve::Known(EcCurve::NistP521),
|
||||
_ => unreachable!("Already validated in match above"),
|
||||
};
|
||||
|
||||
let _curve_identifier = cert_data.read_ssh_string()?;
|
||||
|
||||
let public_key_data = cert_data.read_ssh_bytes()?;
|
||||
SshBasePublicKey::Ec(PublicKey::from_ec_encoded_components(&curve.into(), &public_key_data))
|
||||
}
|
||||
SshCertKeyType::SshEd25519V01 => {
|
||||
let algorithm = NamedEdAlgorithm::Known(EdAlgorithm::Ed25519).into();
|
||||
|
||||
let public_key_data = cert_data.read_ssh_bytes()?;
|
||||
SshBasePublicKey::Ed(PublicKey::from_ed_encoded_components(&algorithm, &public_key_data))
|
||||
}
|
||||
SshCertKeyType::SshDssV01 => {
|
||||
return Err(SshCertificateError::UnsupportedCertificateType(
|
||||
cert_key_type.as_str().to_owned(),
|
||||
));
|
||||
}
|
||||
SshCertKeyType::SkSshSha2Nistp256V01 => {
|
||||
let _curve_identifier = cert_data.read_ssh_string()?;
|
||||
let public_key_data = cert_data.read_ssh_bytes()?;
|
||||
let application = cert_data.read_ssh_string()?;
|
||||
|
||||
SshBasePublicKey::SkEcdsaSha2NistP256 {
|
||||
base_key: PublicKey::from_ec_encoded_components(
|
||||
&NamedEcCurve::Known(EcCurve::NistP256).into(),
|
||||
&public_key_data,
|
||||
),
|
||||
application,
|
||||
}
|
||||
}
|
||||
SshCertKeyType::SkSshEd25519V01 => {
|
||||
let public_key_data = cert_data.read_ssh_bytes()?;
|
||||
let application = cert_data.read_ssh_string()?;
|
||||
|
||||
SshBasePublicKey::SkEd25519 {
|
||||
base_key: PublicKey::from_ed_encoded_components(
|
||||
&NamedEdAlgorithm::Known(EdAlgorithm::Ed25519).into(),
|
||||
&public_key_data,
|
||||
),
|
||||
application,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let serial = cert_data.read_u64::<BigEndian>()?;
|
||||
let cert_type: SshCertType = SshComplexTypeDecode::decode(&mut cert_data)?;
|
||||
|
||||
let key_id = cert_data.read_ssh_string()?;
|
||||
|
||||
let valid_principals: Vec<String> = SshComplexTypeDecode::decode(&mut cert_data)?;
|
||||
|
||||
let valid_after: Timestamp = SshComplexTypeDecode::decode(&mut cert_data)?;
|
||||
let valid_before: Timestamp = SshComplexTypeDecode::decode(&mut cert_data)?;
|
||||
|
||||
let critical_options: Vec<SshCriticalOption> = SshComplexTypeDecode::decode(&mut cert_data)?;
|
||||
|
||||
let extensions: Vec<SshExtension> = SshComplexTypeDecode::decode(&mut cert_data)?;
|
||||
|
||||
let _ = cert_data.read_ssh_bytes()?; // reserved
|
||||
|
||||
// here is public key
|
||||
let signature_key = cert_data.read_ssh_bytes()?;
|
||||
let signature_public_key: SshBasePublicKey = SshComplexTypeDecode::decode(signature_key.as_slice())?;
|
||||
|
||||
let signature = SshSignature::decode(cert_data)?;
|
||||
|
||||
let mut comment = Vec::new();
|
||||
read_until_linebreak(&mut stream, &mut comment)?;
|
||||
let comment = core::str::from_utf8(&comment)?.trim_end().to_owned();
|
||||
|
||||
Ok(SshCertificate {
|
||||
cert_key_type,
|
||||
public_key: SshPublicKey {
|
||||
inner_key: inner_public_key,
|
||||
comment: String::new(),
|
||||
},
|
||||
nonce,
|
||||
serial,
|
||||
cert_type,
|
||||
key_id,
|
||||
valid_principals,
|
||||
valid_after,
|
||||
valid_before,
|
||||
critical_options,
|
||||
extensions,
|
||||
signature_key: SshPublicKey {
|
||||
inner_key: signature_public_key,
|
||||
comment: String::new(),
|
||||
},
|
||||
signature,
|
||||
comment,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::SshReadExt;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[test]
|
||||
fn ssh_string_decode() {
|
||||
let mut cursor = Cursor::new([0, 0, 0, 5, 112, 105, 99, 107, 121].to_vec());
|
||||
|
||||
let ssh_string = cursor.read_ssh_string().unwrap();
|
||||
|
||||
assert_eq!(5, ssh_string.len());
|
||||
assert_eq!("picky".to_owned(), ssh_string);
|
||||
assert_eq!(9, cursor.position());
|
||||
|
||||
let mut cursor = Cursor::new([0, 0, 0, 0].to_vec());
|
||||
|
||||
let ssh_string = cursor.read_ssh_string().unwrap();
|
||||
|
||||
assert_eq!(0, ssh_string.len());
|
||||
assert_eq!("".to_owned(), ssh_string);
|
||||
assert_eq!(4, cursor.position());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn byte_array_decode() {
|
||||
let mut cursor = Cursor::new([0, 0, 0, 5, 1, 2, 3, 4, 5].to_vec());
|
||||
|
||||
let byte_array = cursor.read_ssh_bytes().unwrap();
|
||||
|
||||
assert_eq!(5, byte_array.len());
|
||||
assert_eq!([1, 2, 3, 4, 5].to_vec(), byte_array);
|
||||
assert_eq!(9, cursor.position());
|
||||
|
||||
let mut cursor = Cursor::new([0, 0, 0, 0].to_vec());
|
||||
|
||||
let byte_array = cursor.read_ssh_bytes().unwrap();
|
||||
|
||||
assert_eq!(0, byte_array.len());
|
||||
assert_eq!(Vec::<u8>::new(), byte_array);
|
||||
assert_eq!(4, cursor.position());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mpint_decoding() {
|
||||
let mut cursor = Cursor::new(vec![
|
||||
0x00, 0x00, 0x00, 0x08, 0x09, 0xa3, 0x78, 0xf9, 0xb2, 0xe3, 0x32, 0xa7,
|
||||
]);
|
||||
let mpint = cursor.read_ssh_mpint().unwrap();
|
||||
assert_eq!(
|
||||
mpint.to_be_bytes_trimmed_vartime().as_ref(),
|
||||
&[0x09, 0xa3, 0x78, 0xf9, 0xb2, 0xe3, 0x32, 0xa7]
|
||||
);
|
||||
|
||||
let mut cursor = Cursor::new(vec![0x00, 0x00, 0x00, 0x02, 0x00, 0x80]);
|
||||
let mpint = cursor.read_ssh_mpint().unwrap();
|
||||
assert_eq!(mpint.to_be_bytes_trimmed_vartime().as_ref(), [0x80]);
|
||||
|
||||
let mut cursor = Cursor::new(vec![0x00, 0x00, 0x00, 0x02, 0xed, 0xcc]);
|
||||
let mpint = cursor.read_ssh_mpint().unwrap();
|
||||
assert_eq!(mpint.to_be_bytes_trimmed_vartime().as_ref(), &[0xed, 0xcc]);
|
||||
}
|
||||
}
|
||||
Vendored
+574
@@ -0,0 +1,574 @@
|
||||
use crate::key::ec::{EcdsaKeypair, EcdsaPublicKey};
|
||||
use crate::key::ed::{EdKeypair, EdPublicKey};
|
||||
use crate::ssh::certificate::{
|
||||
SshCertType, SshCertTypeError, SshCertificate, SshCertificateError, SshCriticalOption, SshCriticalOptionError,
|
||||
SshExtension, SshExtensionError, SshSignature, SshSignatureError, Timestamp,
|
||||
};
|
||||
use crate::ssh::private_key::{
|
||||
AES256_CTR, AUTH_MAGIC, Aes256Ctr, BCRYPT, KdfOption, NONE, SshBasePrivateKey, SshPrivateKey, SshPrivateKeyError,
|
||||
};
|
||||
use crate::ssh::public_key::{SshBasePublicKey, SshPublicKey, SshPublicKeyError};
|
||||
use crate::ssh::{Base64Writer, EcCurveSshExt as _, EdAlgorithmSshExt as _, SSH_COMBO_ED25519_KEY_LENGTH, key_type};
|
||||
|
||||
use super::certificate::SshSignatureBlob;
|
||||
use super::key_identifier;
|
||||
use aes::cipher::{KeyIvInit, StreamCipher};
|
||||
use base64::engine::general_purpose;
|
||||
use byteorder::{BigEndian, WriteBytesExt};
|
||||
use crypto_bigint::NonZero;
|
||||
use rsa::traits::{PrivateKeyParts as _, PublicKeyParts as _};
|
||||
use rsa::{BoxedUint, RsaPrivateKey, RsaPublicKey};
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub trait SshWriteExt {
|
||||
type Error;
|
||||
|
||||
fn write_ssh_string(&mut self, data: &str) -> Result<(), Self::Error>;
|
||||
fn write_ssh_bytes(&mut self, data: &[u8]) -> Result<(), Self::Error>;
|
||||
fn write_ssh_mpint(&mut self, data: &BoxedUint) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
impl<T> SshWriteExt for T
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
type Error = io::Error;
|
||||
|
||||
fn write_ssh_string(&mut self, data: &str) -> Result<(), Self::Error> {
|
||||
self.write_u32::<BigEndian>(data.len() as u32)?;
|
||||
self.write_all(data.as_bytes())
|
||||
}
|
||||
|
||||
fn write_ssh_bytes(&mut self, data: &[u8]) -> Result<(), Self::Error> {
|
||||
self.write_u32::<BigEndian>(data.len() as u32)?;
|
||||
self.write_all(data)
|
||||
}
|
||||
|
||||
fn write_ssh_mpint(&mut self, data: &BoxedUint) -> Result<(), Self::Error> {
|
||||
let data = data.to_be_bytes_trimmed_vartime();
|
||||
let size = data.len() as u32;
|
||||
// If the most significant bit would be set for
|
||||
// a positive number, the number MUST be preceded by a zero byte.
|
||||
if size > 0 && data[0] & 0b10000000 != 0 {
|
||||
self.write_u32::<BigEndian>(size + 1)?;
|
||||
self.write_u8(0)?;
|
||||
} else {
|
||||
self.write_u32::<BigEndian>(size)?;
|
||||
}
|
||||
self.write_all(&data)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SshComplexTypeEncode {
|
||||
type Error;
|
||||
|
||||
fn encode(&self, stream: impl Write) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for SshCertType {
|
||||
type Error = SshCertTypeError;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
stream.write_u32::<BigEndian>((*self).into())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for SshCriticalOption {
|
||||
type Error = SshCriticalOptionError;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
stream.write_ssh_string(self.option_type.as_str())?;
|
||||
stream.write_ssh_string(self.data.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SshComplexTypeEncode for Vec<T>
|
||||
where
|
||||
T: SshComplexTypeEncode,
|
||||
T::Error: From<std::io::Error>,
|
||||
{
|
||||
type Error = T::Error;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
let mut data = Vec::new();
|
||||
for elem in self.iter() {
|
||||
elem.encode(&mut data)?;
|
||||
}
|
||||
stream.write_ssh_bytes(&data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for SshExtension {
|
||||
type Error = SshExtensionError;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
stream.write_ssh_string(self.extension_type.as_str())?;
|
||||
stream.write_ssh_string(self.data.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for Vec<String> {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
let mut data = Vec::new();
|
||||
for s in self.iter() {
|
||||
data.write_ssh_string(s)?;
|
||||
}
|
||||
stream.write_ssh_bytes(&data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for SshSignature {
|
||||
type Error = SshSignatureError;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
let overall_size = self.format.as_str().len() + self.blob.size() + 8;
|
||||
stream.write_u32::<BigEndian>(overall_size as u32)?;
|
||||
stream.write_ssh_string(self.format.as_str())?;
|
||||
|
||||
match &self.blob {
|
||||
SshSignatureBlob::Standard(data) => {
|
||||
stream.write_ssh_bytes(data)?;
|
||||
}
|
||||
SshSignatureBlob::Sk { data, flags, counter } => {
|
||||
stream.write_ssh_bytes(data)?;
|
||||
stream.write_u8(*flags)?;
|
||||
stream.write_u32::<BigEndian>(*counter)?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for KdfOption {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
if self.salt.is_empty() {
|
||||
stream.write_u32::<BigEndian>(0)?;
|
||||
return Ok(());
|
||||
}
|
||||
let mut data = Vec::new();
|
||||
data.write_ssh_bytes(&self.salt)?;
|
||||
data.write_u32::<BigEndian>(self.rounds)?;
|
||||
stream.write_ssh_bytes(&data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for Timestamp {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
stream.write_u64::<BigEndian>(self.0)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for SshBasePublicKey {
|
||||
type Error = SshPublicKeyError;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
match self {
|
||||
SshBasePublicKey::Rsa(rsa) => {
|
||||
let rsa = RsaPublicKey::try_from(rsa)?;
|
||||
stream.write_ssh_string(key_type::RSA)?;
|
||||
stream.write_ssh_mpint(rsa.e())?;
|
||||
stream.write_ssh_mpint(rsa.n())?;
|
||||
Ok(())
|
||||
}
|
||||
SshBasePublicKey::Ec(ec) => {
|
||||
let key = EcdsaPublicKey::try_from(ec)?;
|
||||
encode_ecdsa_public_key_body(&mut stream, &key)?;
|
||||
Ok(())
|
||||
}
|
||||
SshBasePublicKey::Ed(ed) => {
|
||||
let key = EdPublicKey::try_from(ed)?;
|
||||
encode_ed_public_key_body(&mut stream, &key)
|
||||
}
|
||||
SshBasePublicKey::SkEcdsaSha2NistP256 { base_key, application } => {
|
||||
let key = EcdsaPublicKey::try_from(base_key)?;
|
||||
|
||||
stream.write_ssh_string(key_type::SK_ECDSA_SHA2_NIST_P256)?;
|
||||
stream.write_ssh_string(key_identifier::ECDSA_SHA2_NIST_P256)?;
|
||||
stream.write_ssh_bytes(key.encoded_point())?;
|
||||
|
||||
stream.write_ssh_string(application.as_str())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
SshBasePublicKey::SkEd25519 { base_key, application } => {
|
||||
let key = EdPublicKey::try_from(base_key)?;
|
||||
|
||||
stream.write_ssh_string(key_type::SK_ED25519)?;
|
||||
stream.write_ssh_bytes(key.data())?;
|
||||
|
||||
stream.write_ssh_string(application.as_str())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for SshPublicKey {
|
||||
type Error = SshPublicKeyError;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
// Write key type
|
||||
match &self.inner_key {
|
||||
SshBasePublicKey::Rsa(_) => {
|
||||
stream.write_all(key_type::RSA.as_bytes())?;
|
||||
}
|
||||
SshBasePublicKey::Ec(key) => {
|
||||
let key = EcdsaPublicKey::try_from(key)?;
|
||||
stream.write_all(key.curve().to_ecdsa_ssh_key_type()?.as_bytes())?;
|
||||
}
|
||||
SshBasePublicKey::Ed(key) => {
|
||||
let key = EdPublicKey::try_from(key)?;
|
||||
stream.write_all(key.algorithm().to_ed_ssh_key_type()?.as_bytes())?;
|
||||
}
|
||||
SshBasePublicKey::SkEcdsaSha2NistP256 { .. } => {
|
||||
stream.write_all(key_type::SK_ECDSA_SHA2_NIST_P256.as_bytes())?;
|
||||
}
|
||||
SshBasePublicKey::SkEd25519 { .. } => {
|
||||
stream.write_all(key_type::SK_ED25519.as_bytes())?;
|
||||
}
|
||||
};
|
||||
|
||||
stream.write_u8(b' ')?;
|
||||
|
||||
{
|
||||
let mut base64_write = Base64Writer::new(&mut stream, &general_purpose::STANDARD);
|
||||
self.inner_key.encode(&mut base64_write)?;
|
||||
base64_write.finish()?;
|
||||
}
|
||||
|
||||
stream.write_u8(b' ')?;
|
||||
stream.write_all(self.comment.as_bytes())?;
|
||||
stream.write_all("\r\n".as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for SshBasePrivateKey {
|
||||
type Error = SshPrivateKeyError;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
match self {
|
||||
SshBasePrivateKey::Rsa(rsa) => {
|
||||
let rsa = RsaPrivateKey::try_from(rsa)?;
|
||||
stream.write_ssh_string(key_type::RSA)?;
|
||||
stream.write_ssh_mpint(rsa.n())?;
|
||||
stream.write_ssh_mpint(rsa.e())?;
|
||||
stream.write_ssh_mpint(rsa.d())?;
|
||||
|
||||
let prime = NonZero::new(rsa.primes()[0].clone())
|
||||
.into_option()
|
||||
.ok_or(SshPrivateKeyError::RsaPrimeIsZero)?;
|
||||
let iqmp = rsa.primes()[1]
|
||||
.invert_mod(&prime)
|
||||
.into_option()
|
||||
.ok_or(SshPrivateKeyError::RsaSecondPrimeInvertModFirstPrimeFailed)?;
|
||||
stream.write_ssh_mpint(&iqmp)?;
|
||||
|
||||
for prime in rsa.primes().iter() {
|
||||
stream.write_ssh_mpint(prime)?;
|
||||
}
|
||||
}
|
||||
SshBasePrivateKey::Ec(key) => {
|
||||
let keypair = EcdsaKeypair::try_from(key)?;
|
||||
|
||||
let public_key = EcdsaPublicKey::try_from(&keypair)?;
|
||||
|
||||
// Encode the public key part
|
||||
encode_ecdsa_public_key_body(&mut stream, &public_key)?;
|
||||
|
||||
// Ecnode encoded secret
|
||||
let secret = BoxedUint::from_be_slice_vartime(keypair.secret());
|
||||
stream.write_ssh_mpint(&secret)?;
|
||||
}
|
||||
SshBasePrivateKey::Ed(key) => {
|
||||
let keypair = EdKeypair::try_from(key)?;
|
||||
let public_key = EdPublicKey::try_from(&keypair)?;
|
||||
encode_ed_public_key_body(&mut stream, &public_key)?;
|
||||
|
||||
// SSH Ed25519 key private kye field contains secret in first 32 bytes and the
|
||||
// public key copy in the last 32 bytes.
|
||||
let mut secret = Vec::with_capacity(SSH_COMBO_ED25519_KEY_LENGTH);
|
||||
secret.extend_from_slice(keypair.secret());
|
||||
secret.extend_from_slice(public_key.data());
|
||||
|
||||
stream.write_ssh_bytes(&secret)?;
|
||||
}
|
||||
SshBasePrivateKey::SkEcdsaSha2NistP256 {
|
||||
public_key,
|
||||
application,
|
||||
flags,
|
||||
handle,
|
||||
} => {
|
||||
let ec_key = EcdsaPublicKey::try_from(public_key)?;
|
||||
|
||||
// Encode the public key part
|
||||
stream.write_ssh_string(key_type::SK_ECDSA_SHA2_NIST_P256)?;
|
||||
stream.write_ssh_string(key_identifier::ECDSA_SHA2_NIST_P256)?;
|
||||
stream.write_ssh_bytes(ec_key.encoded_point())?;
|
||||
|
||||
stream.write_ssh_string(application.as_str())?;
|
||||
stream.write_u8(*flags)?;
|
||||
stream.write_ssh_bytes(handle)?;
|
||||
// Reserved
|
||||
stream.write_ssh_bytes(&[])?;
|
||||
}
|
||||
SshBasePrivateKey::SkEd25519 {
|
||||
public_key,
|
||||
application,
|
||||
flags,
|
||||
handle,
|
||||
} => {
|
||||
let ed_key = EdPublicKey::try_from(public_key)?;
|
||||
|
||||
stream.write_ssh_string(key_type::SK_ED25519)?;
|
||||
stream.write_ssh_bytes(ed_key.data())?;
|
||||
|
||||
stream.write_ssh_string(application.as_str())?;
|
||||
stream.write_u8(*flags)?;
|
||||
stream.write_ssh_bytes(handle)?;
|
||||
// Reserved
|
||||
stream.write_ssh_bytes(&[])?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_ed_public_key_body(mut stream: impl Write, key: &EdPublicKey<'_>) -> Result<(), SshPublicKeyError> {
|
||||
stream.write_ssh_string(key.algorithm().to_ed_ssh_key_type()?)?;
|
||||
stream.write_ssh_bytes(key.data())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encode_ecdsa_public_key_body(mut stream: impl Write, key: &EcdsaPublicKey<'_>) -> Result<(), SshPublicKeyError> {
|
||||
stream.write_ssh_string(key.curve().to_ecdsa_ssh_key_type()?)?;
|
||||
stream.write_ssh_string(key.curve().to_ecdsa_ssh_key_identifier()?)?;
|
||||
|
||||
// So called "Q" value from RFC5656. In fact - standard SEC1 encoded public key representation
|
||||
stream.write_ssh_bytes(key.encoded_point())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for SshPrivateKey {
|
||||
type Error = SshPrivateKeyError;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
const AES256_CTR_BLOCK_SIZE: usize = 16;
|
||||
const UNENCRYPTED_PADDING_SIZE: usize = 8;
|
||||
|
||||
stream.write_all(AUTH_MAGIC.as_bytes())?;
|
||||
stream.write_u8(b'\0')?;
|
||||
|
||||
if self.passphrase.is_some() {
|
||||
stream.write_ssh_string(AES256_CTR)?;
|
||||
stream.write_ssh_string(BCRYPT)?;
|
||||
|
||||
let salt = &self.kdf.option.salt;
|
||||
let rounds = self.kdf.option.rounds;
|
||||
|
||||
let mut kdf_options = Vec::new();
|
||||
kdf_options.write_ssh_bytes(salt)?;
|
||||
kdf_options.write_u32::<BigEndian>(rounds)?;
|
||||
|
||||
stream.write_ssh_bytes(&kdf_options)?;
|
||||
} else {
|
||||
stream.write_ssh_string(NONE)?;
|
||||
stream.write_ssh_string(NONE)?;
|
||||
stream.write_ssh_string("")?;
|
||||
}
|
||||
|
||||
stream.write_u32::<BigEndian>(1)?; // keys amount
|
||||
|
||||
let mut public_key = Vec::new();
|
||||
self.public_key().inner_key.encode(&mut public_key)?;
|
||||
stream.write_ssh_bytes(&public_key)?;
|
||||
|
||||
public_key.clear();
|
||||
let mut private_key = public_key;
|
||||
|
||||
private_key.write_u32::<BigEndian>(self.check)?;
|
||||
private_key.write_u32::<BigEndian>(self.check)?;
|
||||
self.base_key.encode(&mut private_key)?;
|
||||
|
||||
private_key.write_ssh_string(&self.comment)?;
|
||||
|
||||
let padding_size = if self.passphrase.is_some() {
|
||||
AES256_CTR_BLOCK_SIZE
|
||||
} else {
|
||||
UNENCRYPTED_PADDING_SIZE
|
||||
};
|
||||
|
||||
// add padding
|
||||
for i in 1..=(padding_size - (private_key.len() % padding_size)) {
|
||||
private_key.push(i as u8);
|
||||
}
|
||||
|
||||
if let Some(passphrase) = &self.passphrase {
|
||||
// encrypt private_key
|
||||
let n = 48;
|
||||
let mut hash = [0; 48];
|
||||
|
||||
let salt = &self.kdf.option.salt;
|
||||
let rounds = self.kdf.option.rounds;
|
||||
|
||||
bcrypt_pbkdf::bcrypt_pbkdf(passphrase, salt, rounds, &mut hash)?;
|
||||
|
||||
let (key, iv) = hash.split_at(n - 16);
|
||||
let mut cipher = Aes256Ctr::new_from_slices(key, iv).unwrap();
|
||||
|
||||
let private_key_len = private_key.len();
|
||||
private_key.resize(private_key_len + 32, 0u8);
|
||||
cipher.apply_keystream(&mut private_key);
|
||||
private_key.truncate(private_key_len);
|
||||
}
|
||||
|
||||
stream.write_ssh_bytes(&private_key)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for SshCertificate {
|
||||
type Error = SshCertificateError;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
stream.write_all(self.cert_key_type.as_str().as_bytes())?;
|
||||
stream.write_u8(b' ')?;
|
||||
|
||||
let mut cert_data = Base64Writer::new(stream, &general_purpose::STANDARD);
|
||||
|
||||
cert_data.write_ssh_string(self.cert_key_type.as_str())?;
|
||||
cert_data.write_ssh_bytes(&self.nonce)?;
|
||||
match &self.public_key.inner_key {
|
||||
SshBasePublicKey::Rsa(rsa) => {
|
||||
let rsa = RsaPublicKey::try_from(rsa)?;
|
||||
cert_data.write_ssh_mpint(rsa.e())?;
|
||||
cert_data.write_ssh_mpint(rsa.n())?;
|
||||
}
|
||||
SshBasePublicKey::Ec(ec) => {
|
||||
let ec = EcdsaPublicKey::try_from(ec)?;
|
||||
cert_data.write_ssh_string(ec.curve().to_ecdsa_ssh_key_identifier()?)?;
|
||||
cert_data.write_ssh_bytes(ec.encoded_point())?;
|
||||
}
|
||||
SshBasePublicKey::Ed(ed) => {
|
||||
let ed = EdPublicKey::try_from(ed)?;
|
||||
cert_data.write_ssh_bytes(ed.data())?;
|
||||
}
|
||||
SshBasePublicKey::SkEcdsaSha2NistP256 { base_key, application } => {
|
||||
let ec = EcdsaPublicKey::try_from(base_key)?;
|
||||
cert_data.write_ssh_string(key_identifier::ECDSA_SHA2_NIST_P256)?;
|
||||
cert_data.write_ssh_bytes(ec.encoded_point())?;
|
||||
cert_data.write_ssh_string(application.as_str())?;
|
||||
}
|
||||
SshBasePublicKey::SkEd25519 { base_key, application } => {
|
||||
let ed = EdPublicKey::try_from(base_key)?;
|
||||
cert_data.write_ssh_bytes(ed.data())?;
|
||||
cert_data.write_ssh_string(application.as_str())?;
|
||||
}
|
||||
};
|
||||
|
||||
cert_data.write_u64::<BigEndian>(self.serial)?;
|
||||
|
||||
self.cert_type.encode(&mut cert_data)?;
|
||||
|
||||
cert_data.write_ssh_string(self.key_id.as_str())?;
|
||||
|
||||
self.valid_principals.encode(&mut cert_data)?;
|
||||
self.valid_after.encode(&mut cert_data)?;
|
||||
self.valid_before.encode(&mut cert_data)?;
|
||||
self.critical_options.encode(&mut cert_data)?;
|
||||
self.extensions.encode(&mut cert_data)?;
|
||||
|
||||
cert_data.write_ssh_bytes(&[])?; // reserved
|
||||
|
||||
let mut rsa_key = Vec::new();
|
||||
self.signature_key.inner_key.encode(&mut rsa_key)?;
|
||||
|
||||
cert_data.write_ssh_bytes(&rsa_key)?;
|
||||
self.signature.encode(&mut cert_data)?;
|
||||
|
||||
// stream.write_all(cert_data.finish()?.as_slice())?;
|
||||
let mut stream = cert_data.finish().unwrap();
|
||||
stream.write_u8(b' ')?;
|
||||
|
||||
stream.write_all(self.comment.as_bytes())?;
|
||||
stream.write_all("\r\n".as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::SshWriteExt;
|
||||
use rsa::BoxedUint;
|
||||
|
||||
#[test]
|
||||
fn ssh_string_encode() {
|
||||
let mut res = Vec::new();
|
||||
let ssh_string = "picky";
|
||||
|
||||
res.write_ssh_string(ssh_string).unwrap();
|
||||
|
||||
assert_eq!(vec![0, 0, 0, 5, 112, 105, 99, 107, 121], res);
|
||||
|
||||
res.clear();
|
||||
let ssh_string = "";
|
||||
|
||||
res.write_ssh_string(ssh_string).unwrap();
|
||||
|
||||
assert_eq!(vec![0, 0, 0, 0], res);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn byte_array_encode() {
|
||||
let mut res = Vec::new();
|
||||
let byte_array = [1, 2, 3, 4, 5, 6];
|
||||
|
||||
res.write_ssh_bytes(&byte_array).unwrap();
|
||||
|
||||
assert_eq!(vec![0, 0, 0, 6, 1, 2, 3, 4, 5, 6], res);
|
||||
|
||||
res.clear();
|
||||
let byte_array = [];
|
||||
|
||||
res.write_ssh_bytes(&byte_array).unwrap();
|
||||
|
||||
assert_eq!(vec![0, 0, 0, 0], res);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mpint_encoding() {
|
||||
let mpint = BoxedUint::from_be_slice_vartime(&[0x09, 0xa3, 0x78, 0xf9, 0xb2, 0xe3, 0x32, 0xa7]);
|
||||
let mut res = Vec::new();
|
||||
res.write_ssh_mpint(&mpint).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![0x00, 0x00, 0x00, 0x08, 0x09, 0xa3, 0x78, 0xf9, 0xb2, 0xe3, 0x32, 0xa7],
|
||||
);
|
||||
|
||||
let mpint = BoxedUint::from_be_slice_vartime(&[0x80]);
|
||||
let mut res = Vec::new();
|
||||
res.write_ssh_mpint(&mpint).unwrap();
|
||||
|
||||
assert_eq!(res, vec![0x00, 0x00, 0x00, 0x02, 0x00, 0x80]);
|
||||
}
|
||||
}
|
||||
Vendored
+115
@@ -0,0 +1,115 @@
|
||||
pub mod certificate;
|
||||
pub mod decode;
|
||||
pub mod encode;
|
||||
pub mod private_key;
|
||||
pub mod public_key;
|
||||
|
||||
use crate::key::ec::NamedEcCurve;
|
||||
use crate::key::ed::NamedEdAlgorithm;
|
||||
use crate::key::{EcCurve, EdAlgorithm, KeyError};
|
||||
|
||||
use byteorder::ReadBytesExt;
|
||||
use std::io::{self, Read};
|
||||
|
||||
pub use certificate::{SshCertKeyType, SshCertType, SshCertificate, SshCertificateBuilder};
|
||||
pub use private_key::SshPrivateKey;
|
||||
pub use public_key::SshPublicKey;
|
||||
|
||||
pub(crate) type Base64Writer<'a, T, E> = base64::write::EncoderWriter<'a, T, E>;
|
||||
pub(crate) type Base64Reader<'a, T, E> = base64::read::DecoderReader<'a, T, E>;
|
||||
|
||||
const SSH_COMBO_ED25519_KEY_LENGTH: usize = ed25519_dalek::SECRET_KEY_LENGTH + ed25519_dalek::PUBLIC_KEY_LENGTH;
|
||||
|
||||
mod key_type {
|
||||
pub const RSA: &str = "ssh-rsa";
|
||||
pub const ECDSA_SHA2_NIST_P256: &str = "ecdsa-sha2-nistp256";
|
||||
pub const ECDSA_SHA2_NIST_P384: &str = "ecdsa-sha2-nistp384";
|
||||
pub const ECDSA_SHA2_NIST_P521: &str = "ecdsa-sha2-nistp521";
|
||||
pub const ED25519: &str = "ssh-ed25519";
|
||||
pub const SK_ECDSA_SHA2_NIST_P256: &str = "sk-ecdsa-sha2-nistp256@openssh.com";
|
||||
pub const SK_ED25519: &str = "sk-ssh-ed25519@openssh.com";
|
||||
}
|
||||
|
||||
mod key_identifier {
|
||||
pub const ECDSA_SHA2_NIST_P256: &str = "nistp256";
|
||||
pub const ECDSA_SHA2_NIST_P384: &str = "nistp384";
|
||||
pub const ECDSA_SHA2_NIST_P521: &str = "nistp521";
|
||||
}
|
||||
|
||||
trait EcCurveSshExt {
|
||||
fn to_ecdsa_ssh_key_type(&self) -> Result<&'static str, KeyError>;
|
||||
fn to_ecdsa_ssh_key_identifier(&self) -> Result<&'static str, KeyError>;
|
||||
}
|
||||
|
||||
impl EcCurveSshExt for NamedEcCurve {
|
||||
fn to_ecdsa_ssh_key_type(&self) -> Result<&'static str, KeyError> {
|
||||
match self {
|
||||
NamedEcCurve::Known(EcCurve::NistP256) => Ok(key_type::ECDSA_SHA2_NIST_P256),
|
||||
NamedEcCurve::Known(EcCurve::NistP384) => Ok(key_type::ECDSA_SHA2_NIST_P384),
|
||||
NamedEcCurve::Known(EcCurve::NistP521) => Ok(key_type::ECDSA_SHA2_NIST_P521),
|
||||
NamedEcCurve::Unsupported(oid) => Err(KeyError::unsupported_curve(oid, "ssh key type serialization")),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_ecdsa_ssh_key_identifier(&self) -> Result<&'static str, KeyError> {
|
||||
match self {
|
||||
NamedEcCurve::Known(EcCurve::NistP256) => Ok(key_identifier::ECDSA_SHA2_NIST_P256),
|
||||
NamedEcCurve::Known(EcCurve::NistP384) => Ok(key_identifier::ECDSA_SHA2_NIST_P384),
|
||||
NamedEcCurve::Known(EcCurve::NistP521) => Ok(key_identifier::ECDSA_SHA2_NIST_P521),
|
||||
NamedEcCurve::Unsupported(oid) => Err(KeyError::unsupported_curve(oid, "ssh key identifier serialization")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait EdAlgorithmSshExt {
|
||||
fn to_ed_ssh_key_type(&self) -> Result<&'static str, KeyError>;
|
||||
}
|
||||
|
||||
impl EdAlgorithmSshExt for NamedEdAlgorithm {
|
||||
fn to_ed_ssh_key_type(&self) -> Result<&'static str, KeyError> {
|
||||
match self {
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::Ed25519) => Ok(key_type::ED25519),
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::X25519) => Err(KeyError::UnsupportedAlgorithm {
|
||||
algorithm: "X25519 can't be use for SSH EdDSA keys",
|
||||
}),
|
||||
NamedEdAlgorithm::Unsupported(oid) => {
|
||||
Err(KeyError::unsupported_ed_algorithm(oid, "ssh key type serialization"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_until_whitespace(stream: &mut dyn Read, buffer: &mut Vec<u8>) -> io::Result<()> {
|
||||
loop {
|
||||
match stream.read_u8() {
|
||||
Ok(symbol) => {
|
||||
if symbol as char == ' ' {
|
||||
break;
|
||||
} else {
|
||||
buffer.push(symbol);
|
||||
}
|
||||
}
|
||||
Err(ref e) if e.kind() == io::ErrorKind::UnexpectedEof => {
|
||||
break;
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_until_linebreak(stream: &mut dyn Read, buffer: &mut Vec<u8>) -> io::Result<()> {
|
||||
loop {
|
||||
match stream.read_u8() {
|
||||
Ok(b'\r') | Ok(b'\n') => break,
|
||||
Ok(c) => buffer.push(c),
|
||||
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
|
||||
break;
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+780
@@ -0,0 +1,780 @@
|
||||
use crate::key::{EcCurve, EdAlgorithm, KeyError, PrivateKey, PrivateKeyKind, PublicKey};
|
||||
use crate::pem::{Pem, PemError, parse_pem};
|
||||
use crate::ssh::decode::{SshComplexTypeDecode, SshReadExt};
|
||||
use crate::ssh::encode::SshComplexTypeEncode;
|
||||
use crate::ssh::public_key::{SshBasePublicKey, SshPublicKey, SshPublicKeyError};
|
||||
|
||||
use aes::cipher::block_padding::NoPadding;
|
||||
use aes::cipher::{KeyIvInit, StreamCipher};
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use cbc::cipher::BlockModeDecrypt;
|
||||
use rand::RngExt;
|
||||
use std::io::{Cursor, Read};
|
||||
use std::string;
|
||||
use thiserror::Error;
|
||||
|
||||
pub type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
|
||||
pub type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
|
||||
pub type Aes128Ctr = ctr::Ctr32BE<aes::Aes128>;
|
||||
pub type Aes256Ctr = ctr::Ctr32BE<aes::Aes256>;
|
||||
|
||||
const SSH_PRIVATE_KEY_LABEL: &str = "OPENSSH PRIVATE KEY";
|
||||
pub(crate) const AUTH_MAGIC: &str = "openssh-key-v1";
|
||||
|
||||
const AES128_CTR: &str = "aes128-ctr";
|
||||
pub(crate) const AES256_CTR: &str = "aes256-ctr";
|
||||
|
||||
const AES128_CBC: &str = "aes128-cbc";
|
||||
const AES256_CBC: &str = "aes256-cbc";
|
||||
|
||||
pub(crate) const BCRYPT: &str = "bcrypt";
|
||||
pub(crate) const NONE: &str = "none";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SshPrivateKeyError {
|
||||
#[error(transparent)]
|
||||
FromUtf8Error(#[from] string::FromUtf8Error),
|
||||
#[error(transparent)]
|
||||
Base64DecodeError(#[from] base64::DecodeError),
|
||||
#[error(transparent)]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error("Unsupported key type: {0}")]
|
||||
UnsupportedKeyType(String),
|
||||
#[error("Unsupported cipher: {0}")]
|
||||
UnsupportedCipher(String),
|
||||
#[error("Unsupported kdf: {0}")]
|
||||
UnsupportedKdf(String),
|
||||
#[error("Invalid auth magic header")]
|
||||
InvalidAuthMagicHeader,
|
||||
#[error("Invalid keys amount. Expected 1 but got {0}")]
|
||||
InvalidKeysAmount(u32),
|
||||
#[error("Check numbers are not equal: {0} {1}. Wrong passphrase or key is corrupted")]
|
||||
InvalidCheckNumbers(u32, u32),
|
||||
#[error("Invalid public key: {0:?}")]
|
||||
InvalidPublicKey(#[from] SshPublicKeyError),
|
||||
#[error("Invalid key format")]
|
||||
InvalidKeyFormat,
|
||||
#[error("Can not decrypt private key: {0}")]
|
||||
DecryptionError(String),
|
||||
#[error("Can not hash the passphrase: {0:?}")]
|
||||
HashingError(#[from] bcrypt_pbkdf::Error),
|
||||
#[error("Passphrase required for encrypted private key")]
|
||||
MissingPassphrase,
|
||||
#[error(transparent)]
|
||||
KeyError(#[from] KeyError),
|
||||
#[error(transparent)]
|
||||
PemError(#[from] PemError),
|
||||
#[error("RSA prime is zero")]
|
||||
RsaPrimeIsZero,
|
||||
#[error("RSA second prime invert mod first prime failed")]
|
||||
RsaSecondPrimeInvertModFirstPrimeFailed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Default)]
|
||||
pub struct KdfOption {
|
||||
pub salt: Vec<u8>,
|
||||
pub rounds: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct Kdf {
|
||||
pub name: String,
|
||||
pub option: KdfOption,
|
||||
}
|
||||
|
||||
impl Default for Kdf {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: NONE.to_owned(),
|
||||
option: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SshBasePrivateKey {
|
||||
Rsa(PrivateKey),
|
||||
Ec(PrivateKey),
|
||||
Ed(PrivateKey),
|
||||
/// U2F ecdsa SSH key. Note that this key does not contain the private key data, only handle
|
||||
/// is stored which could be used to sign data via hardware U2F key.
|
||||
SkEcdsaSha2NistP256 {
|
||||
public_key: PublicKey,
|
||||
application: String,
|
||||
flags: u8,
|
||||
handle: Vec<u8>,
|
||||
},
|
||||
/// U2F ed25519 SSH key. Note that this key does not contain the private key data, only handle
|
||||
/// is stored which could be used to sign data via hardware U2F key.
|
||||
SkEd25519 {
|
||||
public_key: PublicKey,
|
||||
application: String,
|
||||
flags: u8,
|
||||
handle: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
impl SshBasePrivateKey {
|
||||
pub fn base_public_key(&self) -> Result<SshBasePublicKey, SshPrivateKeyError> {
|
||||
let key = match self {
|
||||
SshBasePrivateKey::Rsa(rsa) => SshBasePublicKey::Rsa(rsa.to_public_key()?),
|
||||
SshBasePrivateKey::Ec(ec) => SshBasePublicKey::Ec(ec.to_public_key()?),
|
||||
SshBasePrivateKey::Ed(ed) => SshBasePublicKey::Ed(ed.to_public_key()?),
|
||||
SshBasePrivateKey::SkEcdsaSha2NistP256 {
|
||||
public_key,
|
||||
application,
|
||||
..
|
||||
} => SshBasePublicKey::SkEcdsaSha2NistP256 {
|
||||
base_key: public_key.clone(),
|
||||
application: application.clone(),
|
||||
},
|
||||
SshBasePrivateKey::SkEd25519 {
|
||||
public_key,
|
||||
application,
|
||||
..
|
||||
} => SshBasePublicKey::SkEd25519 {
|
||||
base_key: public_key.clone(),
|
||||
application: application.clone(),
|
||||
},
|
||||
};
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SshPrivateKey {
|
||||
pub cipher_name: String,
|
||||
pub kdf: Kdf,
|
||||
pub base_key: SshBasePrivateKey,
|
||||
pub public_key: SshPublicKey,
|
||||
pub check: u32,
|
||||
pub comment: String,
|
||||
pub passphrase: Option<String>,
|
||||
}
|
||||
|
||||
impl SshPrivateKey {
|
||||
pub fn generate_rsa(
|
||||
bits: usize,
|
||||
passphrase: Option<String>,
|
||||
comment: Option<String>,
|
||||
) -> Result<Self, SshPrivateKeyError> {
|
||||
SshPrivateKey::h_picky_private_key_to_ssh_private_key(PrivateKey::generate_rsa(bits)?, passphrase, comment)
|
||||
}
|
||||
|
||||
pub fn generate_ec(
|
||||
curve: EcCurve,
|
||||
passphrase: Option<String>,
|
||||
comment: Option<String>,
|
||||
) -> Result<Self, SshPrivateKeyError> {
|
||||
SshPrivateKey::h_picky_private_key_to_ssh_private_key(PrivateKey::generate_ec(curve)?, passphrase, comment)
|
||||
}
|
||||
|
||||
pub fn generate_ed25519(passphrase: Option<String>, comment: Option<String>) -> Result<Self, SshPrivateKeyError> {
|
||||
SshPrivateKey::h_picky_private_key_to_ssh_private_key(
|
||||
PrivateKey::generate_ed(EdAlgorithm::Ed25519, true)?,
|
||||
passphrase,
|
||||
comment,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_pem(pem: &Pem, passphrase: Option<String>) -> Result<Self, SshPrivateKeyError> {
|
||||
SshPrivateKey::decode(&mut pem.data(), passphrase)
|
||||
}
|
||||
|
||||
pub fn from_pem_str(pem: &str, passphrase: Option<String>) -> Result<Self, SshPrivateKeyError> {
|
||||
let pem = parse_pem(pem)?;
|
||||
SshPrivateKey::decode(&mut pem.data(), passphrase)
|
||||
}
|
||||
|
||||
pub fn to_pem(&self) -> Result<Pem<'static>, SshPrivateKeyError> {
|
||||
let mut buffer = Vec::with_capacity(2048);
|
||||
self.encode(&mut buffer)?;
|
||||
Ok(Pem::new(SSH_PRIVATE_KEY_LABEL, buffer))
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> Result<String, SshPrivateKeyError> {
|
||||
let mut buffer = Vec::with_capacity(2048);
|
||||
self.encode(&mut buffer)?;
|
||||
let mut result = Pem::new(SSH_PRIVATE_KEY_LABEL, buffer).to_string();
|
||||
// ssh private key must contain \x0A (\n) character at the end
|
||||
result.push('\x0A');
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> &SshPublicKey {
|
||||
&self.public_key
|
||||
}
|
||||
|
||||
pub fn base_key(&self) -> &SshBasePrivateKey {
|
||||
&self.base_key
|
||||
}
|
||||
|
||||
pub fn inner_key(&self) -> Option<&PrivateKey> {
|
||||
match self.base_key() {
|
||||
SshBasePrivateKey::Rsa(key) => Some(key),
|
||||
SshBasePrivateKey::Ec(key) => Some(key),
|
||||
SshBasePrivateKey::Ed(key) => Some(key),
|
||||
SshBasePrivateKey::SkEcdsaSha2NistP256 { .. } | SshBasePrivateKey::SkEd25519 { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn h_picky_private_key_to_ssh_private_key(
|
||||
private_key: PrivateKey,
|
||||
passphrase: Option<String>,
|
||||
comment: Option<String>,
|
||||
) -> Result<SshPrivateKey, SshPrivateKeyError> {
|
||||
let (kdf, cipher_name) = match &passphrase {
|
||||
Some(_) => {
|
||||
let mut salt: Vec<u8> = Vec::new();
|
||||
let rounds = 16;
|
||||
let mut rnd = rand::rng();
|
||||
for _ in 0..rounds {
|
||||
salt.push(rnd.random::<u8>());
|
||||
}
|
||||
|
||||
let kdf = Kdf {
|
||||
name: BCRYPT.to_owned(),
|
||||
option: KdfOption { salt, rounds },
|
||||
};
|
||||
|
||||
(kdf, String::new())
|
||||
}
|
||||
None => (Kdf::default(), NONE.to_owned()),
|
||||
};
|
||||
|
||||
let public_key = private_key.to_public_key()?;
|
||||
|
||||
let (public, private) = match private_key.as_kind() {
|
||||
PrivateKeyKind::Rsa => {
|
||||
let private = SshBasePrivateKey::Rsa(private_key);
|
||||
let public = SshBasePublicKey::Rsa(public_key);
|
||||
(public, private)
|
||||
}
|
||||
PrivateKeyKind::Ec { .. } => {
|
||||
let private = SshBasePrivateKey::Ec(private_key);
|
||||
let public = SshBasePublicKey::Ec(public_key);
|
||||
(public, private)
|
||||
}
|
||||
PrivateKeyKind::Ed { .. } => {
|
||||
let private = SshBasePrivateKey::Ed(private_key);
|
||||
let public = SshBasePublicKey::Ed(public_key);
|
||||
(public, private)
|
||||
}
|
||||
};
|
||||
|
||||
let public_key = SshPublicKey {
|
||||
inner_key: public,
|
||||
comment: String::new(),
|
||||
};
|
||||
|
||||
Ok(SshPrivateKey {
|
||||
cipher_name,
|
||||
kdf,
|
||||
base_key: private,
|
||||
public_key,
|
||||
check: 0,
|
||||
comment: comment.unwrap_or_default(),
|
||||
passphrase,
|
||||
})
|
||||
}
|
||||
|
||||
fn decode(mut stream: impl Read, passphrase: Option<String>) -> Result<Self, SshPrivateKeyError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let mut auth_magic = [0; AUTH_MAGIC.len()];
|
||||
stream.read_exact(&mut auth_magic)?;
|
||||
if auth_magic != AUTH_MAGIC.as_bytes() {
|
||||
return Err(SshPrivateKeyError::InvalidAuthMagicHeader);
|
||||
}
|
||||
stream.read_u8()?; // skip 1 byte (null-byte)
|
||||
|
||||
let cipher_name = stream.read_ssh_string()?;
|
||||
let kdf_name = stream.read_ssh_string()?;
|
||||
let kdf_option: KdfOption = SshComplexTypeDecode::decode(&mut stream)?;
|
||||
let keys_amount = stream.read_u32::<BigEndian>()?;
|
||||
|
||||
if keys_amount != 1 {
|
||||
return Err(SshPrivateKeyError::InvalidKeysAmount(keys_amount));
|
||||
}
|
||||
|
||||
// read public key
|
||||
let _ = stream.read_ssh_bytes()?;
|
||||
|
||||
// read private key
|
||||
let private_key = stream.read_ssh_bytes()?;
|
||||
|
||||
let data = decrypt(&cipher_name, &kdf_name, &kdf_option, passphrase.as_deref(), private_key)?;
|
||||
|
||||
let mut cursor = Cursor::new(data);
|
||||
|
||||
let check0 = cursor.read_u32::<BigEndian>()?;
|
||||
let check1 = cursor.read_u32::<BigEndian>()?;
|
||||
if check0 != check1 {
|
||||
return Err(SshPrivateKeyError::InvalidCheckNumbers(check0, check1));
|
||||
}
|
||||
|
||||
let base_key: SshBasePrivateKey = SshComplexTypeDecode::decode(&mut cursor)?;
|
||||
let base_public_key = base_key.base_public_key()?;
|
||||
|
||||
let comment = cursor.read_ssh_string()?.trim_end().to_owned();
|
||||
|
||||
Ok(SshPrivateKey {
|
||||
base_key,
|
||||
public_key: SshPublicKey {
|
||||
inner_key: base_public_key,
|
||||
comment: String::new(),
|
||||
},
|
||||
passphrase,
|
||||
kdf: Kdf {
|
||||
name: kdf_name,
|
||||
option: kdf_option,
|
||||
},
|
||||
cipher_name,
|
||||
check: check0,
|
||||
comment,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PrivateKey> for SshPrivateKey {
|
||||
type Error = SshPrivateKeyError;
|
||||
|
||||
fn try_from(private_key: PrivateKey) -> Result<Self, Self::Error> {
|
||||
SshPrivateKey::h_picky_private_key_to_ssh_private_key(private_key, None, None)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn decrypt(
|
||||
cipher_name: &str,
|
||||
kdf_name: &str,
|
||||
kdf_options: &KdfOption,
|
||||
passphrase: Option<&str>,
|
||||
mut data: Vec<u8>,
|
||||
) -> Result<Vec<u8>, SshPrivateKeyError> {
|
||||
if kdf_name == NONE {
|
||||
Ok(data)
|
||||
} else {
|
||||
let n = match cipher_name {
|
||||
AES128_CBC | AES128_CTR => 32,
|
||||
AES256_CBC | AES256_CTR => 48,
|
||||
name => return Err(SshPrivateKeyError::UnsupportedCipher(name.to_owned())),
|
||||
};
|
||||
|
||||
let mut key = [0; 48];
|
||||
match kdf_name {
|
||||
BCRYPT => {
|
||||
let salt = &kdf_options.salt;
|
||||
let rounds = kdf_options.rounds;
|
||||
let passphrase = passphrase.ok_or(SshPrivateKeyError::MissingPassphrase)?;
|
||||
|
||||
bcrypt_pbkdf::bcrypt_pbkdf(passphrase, salt, rounds, &mut key[..n])?;
|
||||
}
|
||||
name => return Err(SshPrivateKeyError::UnsupportedKdf(name.to_owned())),
|
||||
};
|
||||
|
||||
let (key, iv) = key.split_at(n - 16);
|
||||
|
||||
let start_len = data.len();
|
||||
data.resize(data.len() + 32, 0u8);
|
||||
match cipher_name {
|
||||
AES128_CBC => {
|
||||
let cipher = Aes128CbcDec::new_from_slices(key, iv).unwrap();
|
||||
let n = cipher
|
||||
.decrypt_padded_inout::<NoPadding>(data.as_mut_slice().into())
|
||||
.map_err(|e| SshPrivateKeyError::DecryptionError(e.to_string()))?
|
||||
.len();
|
||||
data.truncate(n);
|
||||
Ok(data)
|
||||
}
|
||||
AES256_CBC => {
|
||||
let cipher = Aes256CbcDec::new_from_slices(key, iv).unwrap();
|
||||
let n = cipher
|
||||
.decrypt_padded_inout::<NoPadding>(data.as_mut_slice().into())
|
||||
.map_err(|e| SshPrivateKeyError::DecryptionError(e.to_string()))?
|
||||
.len();
|
||||
data.truncate(n);
|
||||
Ok(data)
|
||||
}
|
||||
AES128_CTR => {
|
||||
let mut cipher = Aes128Ctr::new_from_slices(key, iv).unwrap();
|
||||
cipher.apply_keystream(&mut data);
|
||||
data.truncate(start_len);
|
||||
Ok(data)
|
||||
}
|
||||
AES256_CTR => {
|
||||
let mut cipher = Aes256Ctr::new_from_slices(key, iv).unwrap();
|
||||
cipher.apply_keystream(&mut data);
|
||||
data.truncate(start_len);
|
||||
Ok(data)
|
||||
}
|
||||
name => Err(SshPrivateKeyError::UnsupportedCipher(name.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::key::ec::EcdsaKeypair;
|
||||
use crate::key::ed::EdKeypair;
|
||||
use crate::ssh::private_key::SshPrivateKey;
|
||||
use rsa::RsaPrivateKey;
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn decode_without_passphrase_2048() {
|
||||
// ssh-keygen -t rsa -b 2048 -C "test2@picky.com" (without the passphrase)
|
||||
let ssh_private_key_pem = "-----BEGIN OPENSSH PRIVATE KEY-----\n\
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdz\n\
|
||||
c2gtcnNhAAAAAwEAAQAAAQEAyPYbdoNqjj4EhuYblWIxVKLsmsOff+kLkKlFRsIJ\n\
|
||||
yE5YUWzPm5LyUH3LoqnL/rw/f/Og37oJO/bEn4P2lSvlf6ZagAGaLo8/8ACw4xKY\n\
|
||||
UsQFHAEfreIthd/T2u9TEnN+yPS99M99bXG2tV+6He4c61TJfYrq5DsgQuMXCFmt\n\
|
||||
R/IdJg8qF8lj06qEzjQ1HvXQdXruhm4sQn1HMb3VbdKQFSU3TpmzVysEaOVl3zK7\n\
|
||||
KirBU9gHIOFZuE3y0oUklFuK6jOhjgQnxeo58Rb00g3p7R+YcpI1i95TAoIQ/tYS\n\
|
||||
cjnZzByQv+ak1BjgfOjMbEeEQl6kvi2axqTEnFcg0IHu6wAAA8iqDGUDqgxlAwAA\n\
|
||||
AAdzc2gtcnNhAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhR\n\
|
||||
bM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUc\n\
|
||||
AR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0m\n\
|
||||
DyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT\n\
|
||||
2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnM\n\
|
||||
HJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7rAAAAAwEAAQAAAQATZEw6H2xE\n\
|
||||
1Y8yRTocLCF+fUo/lOjrOt22096veUHgZk73bHyMEp33Tmw8Ag6BQkEOY7/+VsFV\n\
|
||||
W/aVPfKpalb2/mJ1P7JVE9Wjny1ye/Te57NmhGU+LjkeVf7nfXiSqzpswdEisnL0\n\
|
||||
AKkUz2vyP2vi+YeH6cPIyjvOuIMcdyrVakejnGbss19ZoXw660X/7TRqG/41KhTm\n\
|
||||
lkN610JBKI2Rozecx9l3LZ3CTRpOOJ2sfssegvL+qxvvH1YVkRat4dwNZxsi+cho\n\
|
||||
zqWOciXrbzifBghBp0Upe5fgR2JRpyB6sMVXIHKkeP9YBQUARm1ECdbdJmPSiNYP\n\
|
||||
gMKpTaEObMahAAAAgCtugmDSAwIPibrD9MAbJB6KbN15heA6vTtCLOvFe1Hikw94\n\
|
||||
DYAJz+vlKadbOZW5SfGAOuIe7IynafthWm4RcbXEXxhnVtqHxzMHOZo/Mnoh+bUO\n\
|
||||
esDSoERyNHokpNK6m1NKbmQeFj4n7rkcrR8hrwX8+Ng8CsBEglDi+ULtVivbAAAA\n\
|
||||
gQD1vEPRUu9aD7CjkYgDyD2vNRRevARf01ImgT1tpiEA+GLHJ0xMetd7OH0wutAZ\n\
|
||||
uH26V19Kt4sWpsTwfdl2fIw7XHPc+G1OSqiOk6AS9qT/sy/VL1Wn7CqyAN2jikzn\n\
|
||||
quE6MbebTUJQSNHK9vQhn+u4hUDdEoMOLTYdWxxcjdJirQAAAIEA0VsOxBRDSTLc\n\
|
||||
Ar0Y97oCmb/6tU9XGAZwL2E14GVK85PnJNwHrx4aqb0qATE4iPLfE7ms+eBtT8Uj\n\
|
||||
HF0fxM3KDQiFSrvtgM4JjGTDS4dTYIBD/eQ0/aTaRgLOQqplyBgYVr3x7ATfcIP5\n\
|
||||
961TfdiJ/QESutdb1KQquFXIMRII4vcAAAAPdGVzdDJAcGlja3kuY29tAQIDBA==\n\
|
||||
-----END OPENSSH PRIVATE KEY-----";
|
||||
|
||||
let private_key: SshPrivateKey = SshPrivateKey::from_pem_str(ssh_private_key_pem, None).unwrap();
|
||||
|
||||
let kdf = Kdf::default();
|
||||
|
||||
assert_eq!("test2@picky.com".to_owned(), private_key.comment);
|
||||
assert_eq!(kdf, private_key.kdf);
|
||||
assert_eq!("none", private_key.cipher_name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_without_passphrase_4096() {
|
||||
// ssh-keygen -t rsa -b 4096 -C "test@picky.com" (without the passphrase)
|
||||
let ssh_private_key_pem = "-----BEGIN OPENSSH PRIVATE KEY-----b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcnNhAAAAAwEAAQAAAgEA21AiuHR9Z+HThQb/7I3zJmuKuanu0mePY9hjgxiq/A7nmTFmC03JOtblDDJVQU918l+pnul+FrAaIo80Fr4MKSwhk6pYUE57ZuRaYVxx5CsRb4zIT8wpxzUvi9Hm83sHHnLGOa7YMPugYRcHWRoRQX4n9f+rPau8u/vBnt4VCBKi3YjAw88XOusyGltuo2cTuATB7iqe15Z9iXg47ER789LwTQHXTn5L7afoDO9jh+LZvcEv1fG1TmevKFNKLPA7ohBp8AOUZ4zo2hXR1rdZg/Afp0SDcSPM2MkHKqd7eKeedj9Ba4b44IsYuu0cmsdA1DbszdjKUNDkVIEZH8v8VryJlLHj/wX6rzYlpBQFhzQw0rHOdFpq/oNCYnBtoKMBy2D8SkYyyGzqviYMR6xOE3WgNjSaHlKaSYFlOMrhpeX8dRvgXHa9AvpbDI9eB6fmhmoxDi0OzKtx81hKMfRtSoDeK9uujKH3fE+L64xeiWvRPqadKV4BL9nL7WCSz9Knax1mn295VrD+ISVp7/zWlz+mQMYhHh7IoK2PfJJoGWx5v+gJogSe2ykP0vz3pWI95ky9GmJBhe/albQM0pe8iPclch7Je3beY3ZqeviKH7hLTX5wHH6Gki7tDo6LafVQTL4peqI0nGyTSwS/LRjePrqyHLDVL1YwDp8HN56LYSsAAAdIA4ihRQOIoUUAAAAHc3NoLXJzYQAAAgEA21AiuHR9Z+HThQb/7I3zJmuKuanu0mePY9hjgxiq/A7nmTFmC03JOtblDDJVQU918l+pnul+FrAaIo80Fr4MKSwhk6pYUE57ZuRaYVxx5CsRb4zIT8wpxzUvi9Hm83sHHnLGOa7YMPugYRcHWRoRQX4n9f+rPau8u/vBnt4VCBKi3YjAw88XOusyGltuo2cTuATB7iqe15Z9iXg47ER789LwTQHXTn5L7afoDO9jh+LZvcEv1fG1TmevKFNKLPA7ohBp8AOUZ4zo2hXR1rdZg/Afp0SDcSPM2MkHKqd7eKeedj9Ba4b44IsYuu0cmsdA1DbszdjKUNDkVIEZH8v8VryJlLHj/wX6rzYlpBQFhzQw0rHOdFpq/oNCYnBtoKMBy2D8SkYyyGzqviYMR6xOE3WgNjSaHlKaSYFlOMrhpeX8dRvgXHa9AvpbDI9eB6fmhmoxDi0OzKtx81hKMfRtSoDeK9uujKH3fE+L64xeiWvRPqadKV4BL9nL7WCSz9Knax1mn295VrD+ISVp7/zWlz+mQMYhHh7IoK2PfJJoGWx5v+gJogSe2ykP0vz3pWI95ky9GmJBhe/albQM0pe8iPclch7Je3beY3ZqeviKH7hLTX5wHH6Gki7tDo6LafVQTL4peqI0nGyTSwS/LRjePrqyHLDVL1YwDp8HN56LYSsAAAADAQABAAACAC7OXIqnefhIzx7uDoLLDODfRN05Mlo/de/mR967zgo7mBwu2cuBz3e6U2oV9/IXZmHTHt1mkd1/uiQ0Efbkmq3S2FuumGiTR2z/QXbUBw6eTntTPZEiTqxQYpRhuPuv/yX1cu7urP9PRLxT8OKIWLR0m0y6Qy7HT2GDaqBgX3a4m3/SZumjch7GAYx0hRlkr2Wvxj/xYrM6UBKd0PBD8XxpQZX91ZjQBZ50HmdcVA61UKlZ6L6tdneEU3K0y/jpUKDXBfUOnoa3IR8iVwWPXhB1mBvX2IG2FUsTJG9rDUQD6iLsfybWyJkLtrx2TIuQCPsBuep44Tz8SC7s2pLZs0HeihnrM5YmqprMggvZ1TkVFoR3bq/42XO6ULy5k8QPuP6t91UN5iVljgr8H/6Jo9MuCeRA45ZPZN94Cn1mKJWYamrqRuCqDR5za3A0oHPKYUAfzzD90BLL6Yaib75VpiEDTkOiBuW3MJUcJsqZipDDl/6eas2Qyloplw60dx42FzcRIDXkXzRNn8hBSy7xmQ5MOKGBszCeV/eTBtRITQN38yDVMerb8xDlwOsTtjo3PHCg4HEqqSzjv/B0op9aP7RJ8zp9xLOGlxRZ9YhAlHctUOO6ATsv4uCFwCniZbVOdcUEYwNebYQ0x3IRGUF6RpqjOudUwgLlo0Lq1KV05fM5AAABAC7fkAB4l5YMAseu+lcj+CwHySzcI+baRFCrMIKldNjEPvvZcCSOU/n5pgp2bw0ulw8c4mFQv0GsG//qQCBX1IrIWO0/nRBjEUTPIe2BUswoxm3+F7pirphdIpABKMzV7ZvENn53p2ByrW9+uiwwXLo/z4tH18JW41Jyp5mXH2+1iWIYzq5d4gVgMKLGnqWG3DisViHBGg/ExxQCayeXAhlcXVaWZiaVYsgyreaQg58S2RRUIveWP+ZAeb8+ZJ72ZjIYLc0GIbP673GpcNWkRlCykTJXF9x+Ts0trffqvSxF+2YJnaacLSWJmWFU1BsxUO2pIM4SI8VeHYBdEoAVqcQAAAEBAPUodhyNIr8dtcJona8Lkn+3BxLdvYAV1bnlWnUcG9m0RQ2L95kH6folOG00aWhRgJHFDoXcCaHND8Mg3PkAXYUKCucipiIITyd8YeYnF0ckau5GmUEzwc6s4HcGyFilX1yBoyLE7hFMzOJ4+Rcq+zpD2TfaWcuoo+njDWEHeTbzvGIDQoBYsPnGOtw57q9IA5oWYAG3LtwygazmNF2xeEnMEtYPyPu7+W0teO0QIJiHWEuK/yLPOb+RHBfA6YJ1f9Jcgc614DxyW6qnB5YuzQBovLzgp/7j9J4Z9F8n8f9PAwYScf7IG8icVVhl5NwNgfNOpcjdg6+YB8Z0AXa4dYcAAAEBAOUDEl6yS1nwZ0QsJwfHE232dpsOqxxqfV4ei4R8/obq+b5YPHiUgbt2PlHyHtgfQr639BwMmIaAMSR9CLti44Mw6Z3k2DEz3Ef4+XilPeScNiZmWfYanWmVwFEtb2c+YT3QweUH3DUAViHL+UdU7xp+zhkrd04daVPpYc9NNN9b9Gwmj6Pm0RP05UJxsG1ipvN1rGpaCsJiLfS9IoSsKh0Vzdzdty1YvFhEErTl0WBVGGK6xaA5lfMtaclWi2mGGNXfWflyQzkz87eYlPe2RhM7jW1Lo9h1BBYE6R+jKt3q0mHwRehj+updAAXJx0RWF7EDQVJtlTfSrUCm+SSFoD0AAAAOdGVzdEBwaWNreS5jb20BAgMEBQ==-----END OPENSSH PRIVATE KEY-----";
|
||||
|
||||
let private_key: SshPrivateKey = SshPrivateKey::from_pem_str(ssh_private_key_pem, None).unwrap();
|
||||
let kdf = Kdf::default();
|
||||
assert_eq!("test@picky.com".to_owned(), private_key.comment);
|
||||
assert_eq!(kdf, private_key.kdf);
|
||||
assert_eq!("none", private_key.cipher_name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_with_passphrase_2048() {
|
||||
// ssh-keygen -t rsa -b 2048 -C "test_with_pass2@picky.com"
|
||||
let passphrase = Some("123123".to_string());
|
||||
let ssh_private_key_pem = "-----BEGIN OPENSSH PRIVATE KEY-----\n\
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBI\n\
|
||||
MsVovOqXSrZa+iEvQwXzAAAAEAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQABAAAB\n\
|
||||
AQCkR5WaC3NTPZdj9X/bX88YYbR2k5r3aE+I/ezxzbG6xIJi+So9AohypAhReyW9\n\
|
||||
7XSGut5n6a9O+n/c9nCiXFVoyXbMSdM90Av5bu799+V4w3kBlRzN5D3A6uIZRjgl\n\
|
||||
wc3Xso9kthneNByB7OjZuSDdmuWE3YOgmW0TirP3dztbtVScLPZUSsEveIMt90aw\n\
|
||||
uOFWaEUshqb7l713bdEB0Tb77Z1wZpt6UmIgpraV58kN/ahepbY8lirMS4ym75wt\n\
|
||||
Be6PgyKGKIR3aNQdbfHYMHCgxNQMAFUt2yD9f+JE5HWG7kyKDcLTHCY60dtKTNfp\n\
|
||||
cByi4Bwm3209V4rGYSKAzFXvAAAD0CdYEpFE8Dda2GBNy1l5vDNdbyJvx7SSP49l\n\
|
||||
4OmHsgRE2WneNC9CfO2IxPRNXsPEmXimeubqm6alsmJ1Ch+KsdjvyU7WIEnjuonC\n\
|
||||
lLWx6rhsuppJqZSICbikMUXjhlHpLirGnL0WoaBnLYEVYgu8cMbIgE9BNho+bS+1\n\
|
||||
qvyIrIdIblJwwc66CJKUYPz1yRA84WIMZOWlsYfeHnCvTGjiYUG2YFayVAuXvAz/\n\
|
||||
ND3bQYUlO34XOOsJvZxfQNEg1/tzhB7RvcGOG1InoQxT6dZtTp85CkTU/QQ6w2eY\n\
|
||||
j4qDDmsFm/eSDgEFfOJDLrfHsB4+G2aBZLmgk2bn7vo3JBkcPAETX6kKd7bkyEfh\n\
|
||||
LVph9i48vbmNJ8mXWiXMoRXqRgkKqBAMVnuXtbKVDVzzlZXIFu1cbuKyt0zUg7jB\n\
|
||||
IeIdG+5U0L6qygTjOKU6aP+dK1wRc0XyC8jxTJupt2eTEKBLzy4TwlLH5QhEcj1c\n\
|
||||
coV97PyslJ/NnQx8IKflHxxxQF4CbYgyXt9fWZpBfaD9TVWgsFoKrlZ9HOb6s5WJ\n\
|
||||
MwijgNLfllKNkJB/KpQUIwMAqEjkfk4HyKeC9sfCHkjkXoZO28GypRR8Bd5M+/Qf\n\
|
||||
otFvcdRHqbvv+mj1y6nBIv0hv5eqJEil5s/dwGI7cexMGBjPVOPK63kbh6JlMcrb\n\
|
||||
58jKid1VTzUbxxKm6YfL2aQpGp/veGPZRkm+x3DHoANYLYJ64WRQgBOGcf4QSqiT\n\
|
||||
xP9Y5ZxfQuheDzOkiQCt3ToTWwguXtVLm3AAUKxUhHVgMy2PQNFXcNsPWGCzhOW1\n\
|
||||
FzC82iZhuQi7SlTX7iA40np23nMkHu37hkHpfpipySxEIIjv1T0UglqN25hPlHDI\n\
|
||||
jrTRwcBVxikVhP0IFbDUtlmqSP5MkDEE2ZKTeD0ivd8c2WLO5RUoEICaTVHOx+Mx\n\
|
||||
OJ9L07ZhA2NMKiMMqhe0bXwZoFFHMUxXh8+iTTy89oE1PQ7xz/d6hJUtbqJ/N2xp\n\
|
||||
cWMNtnvbjWpxzwhjPGiqKx8GCtpGoAjpUeNqWL9V0a20rJBYqzJGLYfKDd+PW2XT\n\
|
||||
tOHbQwl0DFNq41jP4nYnaFo2YCjWb3mleRUWkU5SoUHq+vUvs4dxqKjlzvKnK5pc\n\
|
||||
yH9bnpKPaBI28QHtye7o25AfkOj7eHVSe5CV4u8okVaBEq1OFhBeWm+jx1fBrk82\n\
|
||||
hEGamuq1GZsZre2y9jauusOFcMXrV5oxJjBLLbGCi0i5ES0O+kBOlB/kY3hdkReC\n\
|
||||
HCJlMN7v92mkSsadahzwx3fTQWCwgVDg6LLN+xCPGFTMts4XDwg=\n\
|
||||
-----END OPENSSH PRIVATE KEY-----";
|
||||
|
||||
let private_key: SshPrivateKey = SshPrivateKey::from_pem_str(ssh_private_key_pem, passphrase).unwrap();
|
||||
|
||||
assert_eq!("test_with_pass2@picky.com".to_owned(), private_key.comment);
|
||||
assert_eq!(
|
||||
Kdf {
|
||||
name: "bcrypt".to_owned(),
|
||||
option: KdfOption {
|
||||
salt: vec![72, 50, 197, 104, 188, 234, 151, 74, 182, 90, 250, 33, 47, 67, 5, 243],
|
||||
rounds: 16,
|
||||
}
|
||||
},
|
||||
private_key.kdf
|
||||
);
|
||||
assert_eq!("aes256-ctr", private_key.cipher_name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_without_passphrase_2048() {
|
||||
// ssh-keygen -t rsa -b 2048 -C "test2@picky.com" (without the passphrase)
|
||||
let ssh_private_key_pem = "-----BEGIN OPENSSH PRIVATE KEY-----\n\
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdz\n\
|
||||
c2gtcnNhAAAAAwEAAQAAAQEAyPYbdoNqjj4EhuYblWIxVKLsmsOff+kLkKlFRsIJ\n\
|
||||
yE5YUWzPm5LyUH3LoqnL/rw/f/Og37oJO/bEn4P2lSvlf6ZagAGaLo8/8ACw4xKY\n\
|
||||
UsQFHAEfreIthd/T2u9TEnN+yPS99M99bXG2tV+6He4c61TJfYrq5DsgQuMXCFmt\n\
|
||||
R/IdJg8qF8lj06qEzjQ1HvXQdXruhm4sQn1HMb3VbdKQFSU3TpmzVysEaOVl3zK7\n\
|
||||
KirBU9gHIOFZuE3y0oUklFuK6jOhjgQnxeo58Rb00g3p7R+YcpI1i95TAoIQ/tYS\n\
|
||||
cjnZzByQv+ak1BjgfOjMbEeEQl6kvi2axqTEnFcg0IHu6wAAA8iqDGUDqgxlAwAA\n\
|
||||
AAdzc2gtcnNhAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhR\n\
|
||||
bM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUc\n\
|
||||
AR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0m\n\
|
||||
DyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT\n\
|
||||
2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnM\n\
|
||||
HJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7rAAAAAwEAAQAAAQATZEw6H2xE\n\
|
||||
1Y8yRTocLCF+fUo/lOjrOt22096veUHgZk73bHyMEp33Tmw8Ag6BQkEOY7/+VsFV\n\
|
||||
W/aVPfKpalb2/mJ1P7JVE9Wjny1ye/Te57NmhGU+LjkeVf7nfXiSqzpswdEisnL0\n\
|
||||
AKkUz2vyP2vi+YeH6cPIyjvOuIMcdyrVakejnGbss19ZoXw660X/7TRqG/41KhTm\n\
|
||||
lkN610JBKI2Rozecx9l3LZ3CTRpOOJ2sfssegvL+qxvvH1YVkRat4dwNZxsi+cho\n\
|
||||
zqWOciXrbzifBghBp0Upe5fgR2JRpyB6sMVXIHKkeP9YBQUARm1ECdbdJmPSiNYP\n\
|
||||
gMKpTaEObMahAAAAgCtugmDSAwIPibrD9MAbJB6KbN15heA6vTtCLOvFe1Hikw94\n\
|
||||
DYAJz+vlKadbOZW5SfGAOuIe7IynafthWm4RcbXEXxhnVtqHxzMHOZo/Mnoh+bUO\n\
|
||||
esDSoERyNHokpNK6m1NKbmQeFj4n7rkcrR8hrwX8+Ng8CsBEglDi+ULtVivbAAAA\n\
|
||||
gQD1vEPRUu9aD7CjkYgDyD2vNRRevARf01ImgT1tpiEA+GLHJ0xMetd7OH0wutAZ\n\
|
||||
uH26V19Kt4sWpsTwfdl2fIw7XHPc+G1OSqiOk6AS9qT/sy/VL1Wn7CqyAN2jikzn\n\
|
||||
quE6MbebTUJQSNHK9vQhn+u4hUDdEoMOLTYdWxxcjdJirQAAAIEA0VsOxBRDSTLc\n\
|
||||
Ar0Y97oCmb/6tU9XGAZwL2E14GVK85PnJNwHrx4aqb0qATE4iPLfE7ms+eBtT8Uj\n\
|
||||
HF0fxM3KDQiFSrvtgM4JjGTDS4dTYIBD/eQ0/aTaRgLOQqplyBgYVr3x7ATfcIP5\n\
|
||||
961TfdiJ/QESutdb1KQquFXIMRII4vcAAAAPdGVzdDJAcGlja3kuY29tAQIDBA==\n\
|
||||
-----END OPENSSH PRIVATE KEY-----\x0A";
|
||||
|
||||
let private_key = SshPrivateKey::from_pem_str(ssh_private_key_pem, None).unwrap();
|
||||
let ssh_private_key_after = private_key.to_string().unwrap();
|
||||
|
||||
pretty_assertions::assert_eq!(ssh_private_key_pem, ssh_private_key_after.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_with_passphrase_2048() {
|
||||
// ssh-keygen -t rsa -b 2048 -C "test_with_pass2@picky.com"
|
||||
let passphrase = Some("123123".to_string());
|
||||
let ssh_private_key_pem = "-----BEGIN OPENSSH PRIVATE KEY-----\n\
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBI\n\
|
||||
MsVovOqXSrZa+iEvQwXzAAAAEAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQABAAAB\n\
|
||||
AQCkR5WaC3NTPZdj9X/bX88YYbR2k5r3aE+I/ezxzbG6xIJi+So9AohypAhReyW9\n\
|
||||
7XSGut5n6a9O+n/c9nCiXFVoyXbMSdM90Av5bu799+V4w3kBlRzN5D3A6uIZRjgl\n\
|
||||
wc3Xso9kthneNByB7OjZuSDdmuWE3YOgmW0TirP3dztbtVScLPZUSsEveIMt90aw\n\
|
||||
uOFWaEUshqb7l713bdEB0Tb77Z1wZpt6UmIgpraV58kN/ahepbY8lirMS4ym75wt\n\
|
||||
Be6PgyKGKIR3aNQdbfHYMHCgxNQMAFUt2yD9f+JE5HWG7kyKDcLTHCY60dtKTNfp\n\
|
||||
cByi4Bwm3209V4rGYSKAzFXvAAAD0CdYEpFE8Dda2GBNy1l5vDNdbyJvx7SSP49l\n\
|
||||
4OmHsgRE2WneNC9CfO2IxPRNXsPEmXimeubqm6alsmJ1Ch+KsdjvyU7WIEnjuonC\n\
|
||||
lLWx6rhsuppJqZSICbikMUXjhlHpLirGnL0WoaBnLYEVYgu8cMbIgE9BNho+bS+1\n\
|
||||
qvyIrIdIblJwwc66CJKUYPz1yRA84WIMZOWlsYfeHnCvTGjiYUG2YFayVAuXvAz/\n\
|
||||
ND3bQYUlO34XOOsJvZxfQNEg1/tzhB7RvcGOG1InoQxT6dZtTp85CkTU/QQ6w2eY\n\
|
||||
j4qDDmsFm/eSDgEFfOJDLrfHsB4+G2aBZLmgk2bn7vo3JBkcPAETX6kKd7bkyEfh\n\
|
||||
LVph9i48vbmNJ8mXWiXMoRXqRgkKqBAMVnuXtbKVDVzzlZXIFu1cbuKyt0zUg7jB\n\
|
||||
IeIdG+5U0L6qygTjOKU6aP+dK1wRc0XyC8jxTJupt2eTEKBLzy4TwlLH5QhEcj1c\n\
|
||||
coV97PyslJ/NnQx8IKflHxxxQF4CbYgyXt9fWZpBfaD9TVWgsFoKrlZ9HOb6s5WJ\n\
|
||||
MwijgNLfllKNkJB/KpQUIwMAqEjkfk4HyKeC9sfCHkjkXoZO28GypRR8Bd5M+/Qf\n\
|
||||
otFvcdRHqbvv+mj1y6nBIv0hv5eqJEil5s/dwGI7cexMGBjPVOPK63kbh6JlMcrb\n\
|
||||
58jKid1VTzUbxxKm6YfL2aQpGp/veGPZRkm+x3DHoANYLYJ64WRQgBOGcf4QSqiT\n\
|
||||
xP9Y5ZxfQuheDzOkiQCt3ToTWwguXtVLm3AAUKxUhHVgMy2PQNFXcNsPWGCzhOW1\n\
|
||||
FzC82iZhuQi7SlTX7iA40np23nMkHu37hkHpfpipySxEIIjv1T0UglqN25hPlHDI\n\
|
||||
jrTRwcBVxikVhP0IFbDUtlmqSP5MkDEE2ZKTeD0ivd8c2WLO5RUoEICaTVHOx+Mx\n\
|
||||
OJ9L07ZhA2NMKiMMqhe0bXwZoFFHMUxXh8+iTTy89oE1PQ7xz/d6hJUtbqJ/N2xp\n\
|
||||
cWMNtnvbjWpxzwhjPGiqKx8GCtpGoAjpUeNqWL9V0a20rJBYqzJGLYfKDd+PW2XT\n\
|
||||
tOHbQwl0DFNq41jP4nYnaFo2YCjWb3mleRUWkU5SoUHq+vUvs4dxqKjlzvKnK5pc\n\
|
||||
yH9bnpKPaBI28QHtye7o25AfkOj7eHVSe5CV4u8okVaBEq1OFhBeWm+jx1fBrk82\n\
|
||||
hEGamuq1GZsZre2y9jauusOFcMXrV5oxJjBLLbGCi0i5ES0O+kBOlB/kY3hdkReC\n\
|
||||
HCJlMN7v92mkSsadahzwx3fTQWCwgVDg6LLN+xCPGFTMts4XDwg=\n\
|
||||
-----END OPENSSH PRIVATE KEY-----\x0A";
|
||||
|
||||
let private_key = SshPrivateKey::from_pem_str(ssh_private_key_pem, passphrase).unwrap();
|
||||
let ssh_private_key_after = private_key.to_string().unwrap();
|
||||
|
||||
pretty_assertions::assert_eq!(ssh_private_key_pem, ssh_private_key_after.as_str());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::SSH_PRIVATE_KEY_EC_P256)]
|
||||
#[case(picky_test_data::SSH_PRIVATE_KEY_EC_P384)]
|
||||
#[case(picky_test_data::SSH_PRIVATE_KEY_EC_P521)]
|
||||
fn ecdsa_keys_unencrypted(#[case] pem: &str) {
|
||||
let key = SshPrivateKey::from_pem_str(pem, None).unwrap();
|
||||
let encoded = key.to_string().unwrap();
|
||||
pretty_assertions::assert_eq!(encoded.as_str(), pem);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecdsa_keys_encrypted() {
|
||||
let passphrase = Some("test".to_string());
|
||||
let key = SshPrivateKey::from_pem_str(picky_test_data::SSH_PRIVATE_KEY_EC_P256_ENCRYPTED, passphrase).unwrap();
|
||||
let encoded = key.to_string().unwrap();
|
||||
pretty_assertions::assert_eq!(encoded.as_str(), picky_test_data::SSH_PRIVATE_KEY_EC_P256_ENCRYPTED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rsa_rounttrip() {
|
||||
let private_key = SshPrivateKey::from_pem_str(picky_test_data::SSH_PRIVATE_KEY_RSA, None).unwrap();
|
||||
let encoded = private_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PRIVATE_KEY_RSA, encoded.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ed25519_roundtrip() {
|
||||
let private_key = SshPrivateKey::from_pem_str(picky_test_data::SSH_PRIVATE_KEY_ED25519, None).unwrap();
|
||||
let encoded = private_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PRIVATE_KEY_ED25519, encoded.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ed25519_roundtrip_encrypted() {
|
||||
let passphrase = Some("test".to_string());
|
||||
let private_key =
|
||||
SshPrivateKey::from_pem_str(picky_test_data::SSH_PRIVATE_KEY_ED25519_ENCRYPTED, passphrase).unwrap();
|
||||
let encoded = private_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PRIVATE_KEY_ED25519_ENCRYPTED, encoded.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sk_ed25519_roundtrip() {
|
||||
let private_key = SshPrivateKey::from_pem_str(picky_test_data::SSH_PRIVATE_KEY_SK_ED25519, None).unwrap();
|
||||
let encoded = private_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PRIVATE_KEY_SK_ED25519, encoded.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sk_ed25519_roundtrip_encrypted() {
|
||||
let private_key = SshPrivateKey::from_pem_str(
|
||||
picky_test_data::SSH_PRIVATE_KEY_SK_ED25519_ENCRYPTED,
|
||||
Some("test".to_string()),
|
||||
)
|
||||
.unwrap();
|
||||
let encoded = private_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PRIVATE_KEY_SK_ED25519_ENCRYPTED, encoded.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sk_ecdsa_roundtrip() {
|
||||
let private_key = SshPrivateKey::from_pem_str(picky_test_data::SSH_PRIVATE_KEY_SK_ECDSA, None).unwrap();
|
||||
let encoded = private_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PRIVATE_KEY_SK_ECDSA, encoded.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sk_ecdsa_roundtrip_encrypted() {
|
||||
let private_key = SshPrivateKey::from_pem_str(
|
||||
picky_test_data::SSH_PRIVATE_KEY_SK_ECDSA_ENCRYPTED,
|
||||
Some("test".to_string()),
|
||||
)
|
||||
.unwrap();
|
||||
let encoded = private_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PRIVATE_KEY_SK_ECDSA_ENCRYPTED, encoded.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rsa_private_key_generation() {
|
||||
let private_key = SshPrivateKey::generate_rsa(2048, Option::Some("123".to_string()), None).unwrap();
|
||||
let data = private_key.to_pem().unwrap();
|
||||
let parsed = SshPrivateKey::from_pem(&data, Option::Some("123".to_string())).unwrap();
|
||||
|
||||
match parsed.base_key() {
|
||||
SshBasePrivateKey::Rsa(key) => {
|
||||
let _rsa: RsaPrivateKey = key.try_into().unwrap();
|
||||
}
|
||||
_ => panic!("Invalid key type"),
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(EcCurve::NistP256)]
|
||||
#[case(EcCurve::NistP384)]
|
||||
#[case(EcCurve::NistP521)]
|
||||
fn test_ec_private_key_generation(#[case] curve: EcCurve) {
|
||||
let private_key = SshPrivateKey::generate_ec(curve, Option::Some("123".to_string()), None).unwrap();
|
||||
let data = private_key.to_pem().unwrap();
|
||||
let parsed = SshPrivateKey::from_pem(&data, Option::Some("123".to_string())).unwrap();
|
||||
|
||||
match parsed.base_key() {
|
||||
SshBasePrivateKey::Ec(ec) => {
|
||||
let _ec: EcdsaKeypair = ec.try_into().unwrap();
|
||||
}
|
||||
_ => panic!("Invalid key type"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ed_private_key_generation() {
|
||||
let private_key = SshPrivateKey::generate_ed25519(Option::Some("123".to_string()), None).unwrap();
|
||||
let data = private_key.to_pem().unwrap();
|
||||
let parsed = SshPrivateKey::from_pem(&data, Option::Some("123".to_string())).unwrap();
|
||||
|
||||
match parsed.base_key() {
|
||||
SshBasePrivateKey::Ed(ed) => {
|
||||
let _ed: EdKeypair = ed.try_into().unwrap();
|
||||
}
|
||||
_ => panic!("Invalid key type"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kdf_option_decode() {
|
||||
let mut cursor = Cursor::new(vec![
|
||||
0, 0, 0, 24, 0, 0, 0, 16, 72, 50, 197, 104, 188, 234, 151, 74, 182, 90, 250, 33, 47, 67, 5, 243, 0, 0, 0,
|
||||
16,
|
||||
]);
|
||||
let kdf_option: KdfOption = SshComplexTypeDecode::decode(&mut cursor).unwrap();
|
||||
let KdfOption { salt, rounds } = kdf_option;
|
||||
|
||||
assert_eq!(
|
||||
vec![72, 50, 197, 104, 188, 234, 151, 74, 182, 90, 250, 33, 47, 67, 5, 243],
|
||||
salt
|
||||
);
|
||||
assert_eq!(16, rounds);
|
||||
|
||||
let mut cursor = Cursor::new(vec![0, 0, 0, 0]);
|
||||
let kdf_option: KdfOption = SshComplexTypeDecode::decode(&mut cursor).unwrap();
|
||||
let KdfOption { salt, rounds } = kdf_option;
|
||||
|
||||
assert!(salt.is_empty());
|
||||
assert_eq!(0, rounds);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kdf_option_encode() {
|
||||
let mut res: Vec<u8> = Vec::new();
|
||||
let kdf_option = KdfOption {
|
||||
salt: vec![72, 50, 197, 104, 188, 234, 151, 74, 182, 90, 250, 33, 47, 67, 5, 243],
|
||||
rounds: 16,
|
||||
};
|
||||
|
||||
kdf_option.encode(&mut res).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
vec![
|
||||
0, 0, 0, 24, 0, 0, 0, 16, 72, 50, 197, 104, 188, 234, 151, 74, 182, 90, 250, 33, 47, 67, 5, 243, 0, 0,
|
||||
0, 16
|
||||
],
|
||||
res
|
||||
);
|
||||
|
||||
res.clear();
|
||||
let kdf_option = KdfOption::default();
|
||||
kdf_option.encode(&mut res).unwrap();
|
||||
|
||||
assert_eq!(vec![0, 0, 0, 0], res);
|
||||
}
|
||||
}
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
use crate::key::{KeyError, PublicKey};
|
||||
use crate::ssh::decode::SshComplexTypeDecode;
|
||||
use crate::ssh::encode::SshComplexTypeEncode;
|
||||
|
||||
use std::io;
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SshPublicKeyError {
|
||||
#[error(transparent)]
|
||||
IoError(#[from] io::Error),
|
||||
#[error("invalid UTF-8")]
|
||||
InvalidUtf8,
|
||||
#[error(transparent)]
|
||||
RsaError(#[from] rsa::errors::Error),
|
||||
#[error(transparent)]
|
||||
Base64DecodeError(#[from] base64::DecodeError),
|
||||
#[error("Unknown key type. We only support RSA")]
|
||||
UnknownKeyType,
|
||||
#[error(transparent)]
|
||||
KeyError(#[from] KeyError),
|
||||
}
|
||||
|
||||
impl From<core::str::Utf8Error> for SshPublicKeyError {
|
||||
fn from(_: core::str::Utf8Error) -> Self {
|
||||
Self::InvalidUtf8
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf8Error> for SshPublicKeyError {
|
||||
fn from(_: std::string::FromUtf8Error) -> Self {
|
||||
Self::InvalidUtf8
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SshBasePublicKey {
|
||||
Rsa(PublicKey),
|
||||
Ec(PublicKey),
|
||||
Ed(PublicKey),
|
||||
/// U2F ecdsa SSH key
|
||||
SkEcdsaSha2NistP256 {
|
||||
base_key: PublicKey,
|
||||
application: String,
|
||||
},
|
||||
/// U2F ed25519 SSH key
|
||||
SkEd25519 {
|
||||
base_key: PublicKey,
|
||||
application: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SshPublicKey {
|
||||
pub inner_key: SshBasePublicKey,
|
||||
pub comment: String,
|
||||
}
|
||||
|
||||
impl SshPublicKey {
|
||||
pub fn to_string(&self) -> Result<String, SshPublicKeyError> {
|
||||
let mut buffer = Vec::with_capacity(1024);
|
||||
self.encode(&mut buffer)?;
|
||||
Ok(String::from_utf8(buffer)?)
|
||||
}
|
||||
|
||||
pub fn inner_key(&self) -> &PublicKey {
|
||||
match &self.inner_key {
|
||||
SshBasePublicKey::Rsa(key) => key,
|
||||
SshBasePublicKey::Ec(key) => key,
|
||||
SshBasePublicKey::Ed(key) => key,
|
||||
SshBasePublicKey::SkEcdsaSha2NistP256 { base_key, .. } => base_key,
|
||||
SshBasePublicKey::SkEd25519 { base_key, .. } => base_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fingerprint_md5(&self) -> Result<[u8; 16], SshPublicKeyError> {
|
||||
use md5::{Digest, Md5};
|
||||
|
||||
let mut encoded = Vec::new();
|
||||
self.inner_key.encode(&mut encoded)?;
|
||||
|
||||
let mut hasher = Md5::new();
|
||||
hasher.update(&encoded);
|
||||
let fingerprint = hasher.finalize();
|
||||
|
||||
Ok(fingerprint.into())
|
||||
}
|
||||
|
||||
pub fn fingerprint_sha1(&self) -> Result<[u8; 20], SshPublicKeyError> {
|
||||
use sha1::{Digest, Sha1};
|
||||
|
||||
let mut encoded = Vec::new();
|
||||
self.inner_key.encode(&mut encoded)?;
|
||||
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(&encoded);
|
||||
let fingerprint = hasher.finalize();
|
||||
|
||||
Ok(fingerprint.into())
|
||||
}
|
||||
|
||||
pub fn fingerprint_sha256(&self) -> Result<[u8; 32], SshPublicKeyError> {
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
let mut encoded = Vec::new();
|
||||
self.inner_key.encode(&mut encoded)?;
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&encoded);
|
||||
let fingerprint = hasher.finalize();
|
||||
|
||||
Ok(fingerprint.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SshPublicKey {
|
||||
type Err = SshPublicKeyError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
SshComplexTypeDecode::decode(s.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD_NO_PAD;
|
||||
use crypto_bigint::BoxedUint;
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn decode_ssh_rsa_4096_public_key() {
|
||||
// ssh-keygen -t rsa -b 4096 -C "test@picky.com"
|
||||
let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDbUCK4dH1n4dOFBv/sjfMma4q5qe7SZ49j2GODGKr8DueZMWYLTck61uUMMlVBT3XyX6me6X4WsBoijzQWvgwpLCGTqlhQTntm5FphXHHkKxFvjMhPzCnHNS+L0ebzewcecsY5rtgw+6BhFwdZGhFBfif1/6s9q7y7+8Ge3hUIEqLdiMDDzxc66zIaW26jZxO4BMHuKp7Xln2JeDjsRHvz0vBNAddOfkvtp+gM72OH4tm9wS/V8bVOZ68oU0os8DuiEGnwA5RnjOjaFdHWt1mD8B+nRINxI8zYyQcqp3t4p552P0Frhvjgixi67Ryax0DUNuzN2MpQ0ORUgRkfy/xWvImUseP/BfqvNiWkFAWHNDDSsc50Wmr+g0JicG2gowHLYPxKRjLIbOq+JgxHrE4TdaA2NJoeUppJgWU4yuGl5fx1G+Bcdr0C+lsMj14Hp+aGajEOLQ7Mq3HzWEox9G1KgN4r266Mofd8T4vrjF6Ja9E+pp0pXgEv2cvtYJLP0qdrHWafb3lWsP4hJWnv/NaXP6ZAxiEeHsigrY98kmgZbHm/6AmiBJ7bKQ/S/PelYj3mTL0aYkGF79qVtAzSl7yI9yVyHsl7dt5jdmp6+IofuEtNfnAcfoaSLu0Ojotp9VBMvil6ojScbJNLBL8tGN4+urIcsNUvVjAOnwc3nothKw== test@picky.com\r\n";
|
||||
|
||||
let public_key = SshPublicKey::from_str(ssh_public_key).unwrap();
|
||||
|
||||
assert_eq!("test@picky.com".to_owned(), public_key.comment);
|
||||
assert_eq!(
|
||||
SshBasePublicKey::Rsa(PublicKey::from_rsa_components(
|
||||
&BoxedUint::from_be_slice_vartime(&[
|
||||
219, 80, 34, 184, 116, 125, 103, 225, 211, 133, 6, 255, 236, 141, 243, 38, 107, 138, 185, 169, 238,
|
||||
210, 103, 143, 99, 216, 99, 131, 24, 170, 252, 14, 231, 153, 49, 102, 11, 77, 201, 58, 214, 229,
|
||||
12, 50, 85, 65, 79, 117, 242, 95, 169, 158, 233, 126, 22, 176, 26, 34, 143, 52, 22, 190, 12, 41,
|
||||
44, 33, 147, 170, 88, 80, 78, 123, 102, 228, 90, 97, 92, 113, 228, 43, 17, 111, 140, 200, 79, 204,
|
||||
41, 199, 53, 47, 139, 209, 230, 243, 123, 7, 30, 114, 198, 57, 174, 216, 48, 251, 160, 97, 23, 7,
|
||||
89, 26, 17, 65, 126, 39, 245, 255, 171, 61, 171, 188, 187, 251, 193, 158, 222, 21, 8, 18, 162, 221,
|
||||
136, 192, 195, 207, 23, 58, 235, 50, 26, 91, 110, 163, 103, 19, 184, 4, 193, 238, 42, 158, 215,
|
||||
150, 125, 137, 120, 56, 236, 68, 123, 243, 210, 240, 77, 1, 215, 78, 126, 75, 237, 167, 232, 12,
|
||||
239, 99, 135, 226, 217, 189, 193, 47, 213, 241, 181, 78, 103, 175, 40, 83, 74, 44, 240, 59, 162,
|
||||
16, 105, 240, 3, 148, 103, 140, 232, 218, 21, 209, 214, 183, 89, 131, 240, 31, 167, 68, 131, 113,
|
||||
35, 204, 216, 201, 7, 42, 167, 123, 120, 167, 158, 118, 63, 65, 107, 134, 248, 224, 139, 24, 186,
|
||||
237, 28, 154, 199, 64, 212, 54, 236, 205, 216, 202, 80, 208, 228, 84, 129, 25, 31, 203, 252, 86,
|
||||
188, 137, 148, 177, 227, 255, 5, 250, 175, 54, 37, 164, 20, 5, 135, 52, 48, 210, 177, 206, 116, 90,
|
||||
106, 254, 131, 66, 98, 112, 109, 160, 163, 1, 203, 96, 252, 74, 70, 50, 200, 108, 234, 190, 38, 12,
|
||||
71, 172, 78, 19, 117, 160, 54, 52, 154, 30, 82, 154, 73, 129, 101, 56, 202, 225, 165, 229, 252,
|
||||
117, 27, 224, 92, 118, 189, 2, 250, 91, 12, 143, 94, 7, 167, 230, 134, 106, 49, 14, 45, 14, 204,
|
||||
171, 113, 243, 88, 74, 49, 244, 109, 74, 128, 222, 43, 219, 174, 140, 161, 247, 124, 79, 139, 235,
|
||||
140, 94, 137, 107, 209, 62, 166, 157, 41, 94, 1, 47, 217, 203, 237, 96, 146, 207, 210, 167, 107,
|
||||
29, 102, 159, 111, 121, 86, 176, 254, 33, 37, 105, 239, 252, 214, 151, 63, 166, 64, 198, 33, 30,
|
||||
30, 200, 160, 173, 143, 124, 146, 104, 25, 108, 121, 191, 232, 9, 162, 4, 158, 219, 41, 15, 210,
|
||||
252, 247, 165, 98, 61, 230, 76, 189, 26, 98, 65, 133, 239, 218, 149, 180, 12, 210, 151, 188, 136,
|
||||
247, 37, 114, 30, 201, 123, 118, 222, 99, 118, 106, 122, 248, 138, 31, 184, 75, 77, 126, 112, 28,
|
||||
126, 134, 146, 46, 237, 14, 142, 139, 105, 245, 80, 76, 190, 41, 122, 162, 52, 156, 108, 147, 75,
|
||||
4, 191, 45, 24, 222, 62, 186, 178, 28, 176, 213, 47, 86, 48, 14, 159, 7, 55, 158, 139, 97, 43
|
||||
]),
|
||||
&BoxedUint::from_be_slice_vartime(&[1, 0, 1])
|
||||
)),
|
||||
public_key.inner_key
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_ssh_rsa_2048_public_key() {
|
||||
// ssh-keygen -t rsa -b 2048 -C "test2@picky.com"
|
||||
let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhRbM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUcAR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0mDyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnMHJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7r test2@picky.com\r\n";
|
||||
|
||||
let public_key: SshPublicKey = SshPublicKey::from_str(ssh_public_key).unwrap();
|
||||
|
||||
assert_eq!("test2@picky.com".to_owned(), public_key.comment);
|
||||
assert_eq!(
|
||||
SshBasePublicKey::Rsa(PublicKey::from_rsa_components(
|
||||
&BoxedUint::from_be_slice_vartime(&[
|
||||
200, 246, 27, 118, 131, 106, 142, 62, 4, 134, 230, 27, 149, 98, 49, 84, 162, 236, 154, 195, 159,
|
||||
127, 233, 11, 144, 169, 69, 70, 194, 9, 200, 78, 88, 81, 108, 207, 155, 146, 242, 80, 125, 203,
|
||||
162, 169, 203, 254, 188, 63, 127, 243, 160, 223, 186, 9, 59, 246, 196, 159, 131, 246, 149, 43, 229,
|
||||
127, 166, 90, 128, 1, 154, 46, 143, 63, 240, 0, 176, 227, 18, 152, 82, 196, 5, 28, 1, 31, 173, 226,
|
||||
45, 133, 223, 211, 218, 239, 83, 18, 115, 126, 200, 244, 189, 244, 207, 125, 109, 113, 182, 181,
|
||||
95, 186, 29, 238, 28, 235, 84, 201, 125, 138, 234, 228, 59, 32, 66, 227, 23, 8, 89, 173, 71, 242,
|
||||
29, 38, 15, 42, 23, 201, 99, 211, 170, 132, 206, 52, 53, 30, 245, 208, 117, 122, 238, 134, 110, 44,
|
||||
66, 125, 71, 49, 189, 213, 109, 210, 144, 21, 37, 55, 78, 153, 179, 87, 43, 4, 104, 229, 101, 223,
|
||||
50, 187, 42, 42, 193, 83, 216, 7, 32, 225, 89, 184, 77, 242, 210, 133, 36, 148, 91, 138, 234, 51,
|
||||
161, 142, 4, 39, 197, 234, 57, 241, 22, 244, 210, 13, 233, 237, 31, 152, 114, 146, 53, 139, 222,
|
||||
83, 2, 130, 16, 254, 214, 18, 114, 57, 217, 204, 28, 144, 191, 230, 164, 212, 24, 224, 124, 232,
|
||||
204, 108, 71, 132, 66, 94, 164, 190, 45, 154, 198, 164, 196, 156, 87, 32, 208, 129, 238, 235
|
||||
]),
|
||||
&BoxedUint::from_be_slice_vartime(&[1, 0, 1])
|
||||
)),
|
||||
public_key.inner_key
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_ssh_rsa_4096_public_key() {
|
||||
// ssh-keygen -t rsa -b 4096 -C "test@picky.com"
|
||||
let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDbUCK4dH1n4dOFBv/sjfMma4q5qe7SZ49j2GODGKr8DueZMWYLTck61uUMMlVBT3XyX6me6X4WsBoijzQWvgwpLCGTqlhQTntm5FphXHHkKxFvjMhPzCnHNS+L0ebzewcecsY5rtgw+6BhFwdZGhFBfif1/6s9q7y7+8Ge3hUIEqLdiMDDzxc66zIaW26jZxO4BMHuKp7Xln2JeDjsRHvz0vBNAddOfkvtp+gM72OH4tm9wS/V8bVOZ68oU0os8DuiEGnwA5RnjOjaFdHWt1mD8B+nRINxI8zYyQcqp3t4p552P0Frhvjgixi67Ryax0DUNuzN2MpQ0ORUgRkfy/xWvImUseP/BfqvNiWkFAWHNDDSsc50Wmr+g0JicG2gowHLYPxKRjLIbOq+JgxHrE4TdaA2NJoeUppJgWU4yuGl5fx1G+Bcdr0C+lsMj14Hp+aGajEOLQ7Mq3HzWEox9G1KgN4r266Mofd8T4vrjF6Ja9E+pp0pXgEv2cvtYJLP0qdrHWafb3lWsP4hJWnv/NaXP6ZAxiEeHsigrY98kmgZbHm/6AmiBJ7bKQ/S/PelYj3mTL0aYkGF79qVtAzSl7yI9yVyHsl7dt5jdmp6+IofuEtNfnAcfoaSLu0Ojotp9VBMvil6ojScbJNLBL8tGN4+urIcsNUvVjAOnwc3nothKw== test@picky.com\r\n";
|
||||
let public_key = SshPublicKey::from_str(ssh_public_key).unwrap();
|
||||
|
||||
let ssh_public_key_after = public_key.to_string().unwrap();
|
||||
|
||||
assert_eq!(ssh_public_key, ssh_public_key_after.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_ssh_rsa_2048_public_key() {
|
||||
// ssh-keygen -t rsa -b 4096 -C "test@picky.com"
|
||||
let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhRbM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUcAR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0mDyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnMHJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7r test2@picky.com\r\n";
|
||||
let public_key = SshPublicKey::from_str(ssh_public_key).unwrap();
|
||||
|
||||
let ssh_public_key_after = public_key.to_string().unwrap();
|
||||
|
||||
assert_eq!(ssh_public_key, ssh_public_key_after.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rsa_roundtrip() {
|
||||
let public_key = SshPublicKey::from_str(picky_test_data::SSH_PUBLIC_KEY_RSA).unwrap();
|
||||
let ssh_public_key_after = public_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PUBLIC_KEY_RSA, ssh_public_key_after.as_str());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::SSH_PUBLIC_KEY_EC_P256)]
|
||||
#[case(picky_test_data::SSH_PUBLIC_KEY_EC_P384)]
|
||||
#[case(picky_test_data::SSH_PUBLIC_KEY_EC_P521)]
|
||||
fn ecdsa_roundtrip(#[case] key_str: &str) {
|
||||
let public_key = SshPublicKey::from_str(key_str).unwrap();
|
||||
let ssh_public_key_after = public_key.to_string().unwrap();
|
||||
assert_eq!(key_str, ssh_public_key_after.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ed25519_roundtrip() {
|
||||
let public_key = SshPublicKey::from_str(picky_test_data::SSH_PUBLIC_KEY_ED25519).unwrap();
|
||||
let ssh_public_key_after = public_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PUBLIC_KEY_ED25519, ssh_public_key_after.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sk_ed25519_roundtrip() {
|
||||
let public_key: SshPublicKey = SshPublicKey::from_str(picky_test_data::SSH_PUBLIC_KEY_SK_ED25519).unwrap();
|
||||
let ssh_public_key_after = public_key.to_string().unwrap();
|
||||
assert_eq!(
|
||||
picky_test_data::SSH_PUBLIC_KEY_SK_ED25519,
|
||||
ssh_public_key_after.as_str()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sk_ecdsa_roundtrip() {
|
||||
let public_key = SshPublicKey::from_str(picky_test_data::SSH_PUBLIC_KEY_SK_ECDSA).unwrap();
|
||||
let ssh_public_key_after = public_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PUBLIC_KEY_SK_ECDSA, ssh_public_key_after.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_md5_ssh_rsa_2048_public_key() {
|
||||
let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhRbM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUcAR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0mDyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnMHJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7r test2@picky.com\r\n";
|
||||
|
||||
let public_key: SshPublicKey = SshPublicKey::from_str(ssh_public_key).unwrap();
|
||||
let md5 = hex::encode(public_key.fingerprint_md5().unwrap());
|
||||
|
||||
assert_eq!(md5, "7b6b9cc2e44452aec58c3a0a31d6258d");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_sha1_ssh_rsa_2048_public_key() {
|
||||
let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhRbM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUcAR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0mDyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnMHJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7r test2@picky.com\r\n";
|
||||
|
||||
let public_key: SshPublicKey = SshPublicKey::from_str(ssh_public_key).unwrap();
|
||||
let sha1 = STANDARD_NO_PAD.encode(public_key.fingerprint_sha1().unwrap());
|
||||
|
||||
assert_eq!(sha1, "ezHoULh4V/R9NybfxCW2pL9ADcU");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_sha256_ssh_rsa_2048_public_key() {
|
||||
let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhRbM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUcAR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0mDyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnMHJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7r test2@picky.com\r\n";
|
||||
|
||||
let public_key: SshPublicKey = SshPublicKey::from_str(ssh_public_key).unwrap();
|
||||
|
||||
let sha256 = STANDARD_NO_PAD.encode(public_key.fingerprint_sha256().unwrap());
|
||||
|
||||
assert_eq!(sha256, "cTXkM4frGl07u46Bhzy+YMOS01lX51oE2j6STi7g568");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_ssh_rsa_2024_public_key_with_multiwords_comment() {
|
||||
// ssh-keygen -t rsa -b 2048 -C "test using several words"
|
||||
let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+76yL1+ocu4iuVam4VO5YlODohmHbIuhjyQgiMGS8ZtdFKcltzAH0ot4zJDH/Z3ja6xO2IOc/4+UMABgPOgFwmzyl414/Zo42CYqk9OB5GJylFYI99HrATqH03Wz2qJ3dzP6QJVf8g05hY27RKaU5H+0fo471SACeHet9uqstRecsUcauPS91xwpPhrcpRXjGH1yLBdWTpDq5R6c1Wgh9SVuzY/ITMB3pq8rzwal8e2rR4T+wHc48l61LGwmuOTkhAo5/0sn72CzKWQZVd0CarfCr3biCW7cUai0FvH79aAfIBV/FIMXgtgqdpY/Qg7v+JWIyJk/OB8Be1ix8YVRV test using several words\r\n";
|
||||
|
||||
let public_key = SshPublicKey::from_str(ssh_public_key).unwrap();
|
||||
|
||||
assert_eq!(public_key.comment, "test using several words");
|
||||
}
|
||||
}
|
||||
Vendored
+131
@@ -0,0 +1,131 @@
|
||||
// TODO: support `SshTime` without `chrono` nor `time`
|
||||
#[cfg(not(any(feature = "chrono_conversion", feature = "time_conversion")))]
|
||||
compile_error!(
|
||||
"Either feature \"chrono_conversion\" or \"time_conversion\" must be enabled when the feature \"ssh\" is set."
|
||||
);
|
||||
|
||||
pub use time_impl::SshTime;
|
||||
|
||||
#[cfg(feature = "time_conversion")]
|
||||
mod time_impl {
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct SshTime(pub(crate) OffsetDateTime);
|
||||
|
||||
impl SshTime {
|
||||
pub fn now() -> Self {
|
||||
Self(OffsetDateTime::now_utc())
|
||||
}
|
||||
|
||||
pub fn from_timestamp(timestamp: u64) -> Self {
|
||||
Self(OffsetDateTime::from_unix_timestamp(timestamp as i64).unwrap())
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> u64 {
|
||||
self.0.unix_timestamp() as u64
|
||||
}
|
||||
|
||||
pub fn month(&self) -> u8 {
|
||||
u8::from(self.0.month())
|
||||
}
|
||||
|
||||
pub fn day(&self) -> u8 {
|
||||
self.0.day()
|
||||
}
|
||||
|
||||
pub fn hour(&self) -> u8 {
|
||||
self.0.hour()
|
||||
}
|
||||
|
||||
pub fn minute(&self) -> u8 {
|
||||
self.0.minute()
|
||||
}
|
||||
|
||||
pub fn second(&self) -> u8 {
|
||||
self.0.second()
|
||||
}
|
||||
|
||||
pub fn year(&self) -> u16 {
|
||||
self.0.year().try_into().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SshTime> for OffsetDateTime {
|
||||
fn from(time: SshTime) -> Self {
|
||||
time.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OffsetDateTime> for SshTime {
|
||||
fn from(time: OffsetDateTime) -> Self {
|
||||
Self::from_timestamp(time.unix_timestamp() as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SshTime> for u64 {
|
||||
fn from(time: SshTime) -> u64 {
|
||||
time.0.unix_timestamp() as u64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "chrono_conversion", not(feature = "time_conversion")))]
|
||||
mod time_impl {
|
||||
use chrono::{DateTime, Utc};
|
||||
pub use chrono::{Datelike, Timelike};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct SshTime(pub(crate) DateTime<Utc>);
|
||||
|
||||
impl SshTime {
|
||||
pub fn now() -> Self {
|
||||
SshTime(DateTime::<Utc>::from(SystemTime::now()))
|
||||
}
|
||||
|
||||
pub fn from_timestamp(timestamp: u64) -> Self {
|
||||
Self(DateTime::<Utc>::from(UNIX_EPOCH + Duration::from_secs(timestamp)))
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> u64 {
|
||||
self.0.timestamp() as u64
|
||||
}
|
||||
|
||||
pub fn month(&self) -> u8 {
|
||||
self.0.month().try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn day(&self) -> u8 {
|
||||
self.0.day().try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn hour(&self) -> u8 {
|
||||
self.0.hour().try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn minute(&self) -> u8 {
|
||||
self.0.minute().try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn second(&self) -> u8 {
|
||||
self.0.second().try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn year(&self) -> u16 {
|
||||
self.0.year().try_into().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DateTime<Utc>> for SshTime {
|
||||
fn from(date: DateTime<Utc>) -> Self {
|
||||
Self(date)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SshTime> for DateTime<Utc> {
|
||||
fn from(time: SshTime) -> Self {
|
||||
time.0
|
||||
}
|
||||
}
|
||||
}
|
||||
+1677
File diff suppressed because it is too large
Load Diff
Vendored
+182
@@ -0,0 +1,182 @@
|
||||
use super::utils::{from_der, from_pem, from_pem_str, to_der, to_pem};
|
||||
use crate::key::{KeyError, PrivateKey, PublicKey};
|
||||
use crate::pem::{Pem, PemError};
|
||||
use crate::signature::{SignatureAlgorithm, SignatureError};
|
||||
use crate::x509::certificate::CertError;
|
||||
use crate::x509::name::DirectoryName;
|
||||
use picky_asn1::bit_string::BitString;
|
||||
use picky_asn1_der::Asn1DerError;
|
||||
use picky_asn1_x509::{CertificationRequest, CertificationRequestInfo};
|
||||
use thiserror::Error;
|
||||
|
||||
pub use picky_asn1_x509::Attribute;
|
||||
|
||||
const ELEMENT_NAME: &str = "certification request";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CsrError {
|
||||
/// ASN1 serialization error
|
||||
#[error("(ASN1) couldn't serialize {element}: {source}")]
|
||||
Asn1Serialization {
|
||||
element: &'static str,
|
||||
source: Asn1DerError,
|
||||
},
|
||||
|
||||
/// ASN1 deserialization error
|
||||
#[error("(ASN1) couldn't deserialize {}: {}", element, source)]
|
||||
Asn1Deserialization {
|
||||
element: &'static str,
|
||||
source: Asn1DerError,
|
||||
},
|
||||
|
||||
/// signature error
|
||||
#[error("signature error: {}", source)]
|
||||
Signature { source: SignatureError },
|
||||
|
||||
/// invalid PEM label error
|
||||
#[error("invalid PEM label: {}", label)]
|
||||
InvalidPemLabel { label: String },
|
||||
|
||||
/// invalid PEM provided
|
||||
#[error("invalid PEM provided: {source}")]
|
||||
Pem { source: PemError },
|
||||
|
||||
#[error("failed to get public key from private key: {source}")]
|
||||
PrivateKeyToPublicKey { source: KeyError },
|
||||
}
|
||||
|
||||
impl From<CertError> for CsrError {
|
||||
fn from(e: CertError) -> Self {
|
||||
match e {
|
||||
CertError::Asn1Deserialization { element, source } => CsrError::Asn1Deserialization { element, source },
|
||||
CertError::Asn1Serialization { element, source } => CsrError::Asn1Serialization { element, source },
|
||||
CertError::Pem { source } => CsrError::Pem { source },
|
||||
CertError::InvalidPemLabel { label } => CsrError::InvalidPemLabel { label },
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CSR_PEM_LABEL: &str = "CERTIFICATE REQUEST";
|
||||
|
||||
/// Certificate Signing Request
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Csr(pub(crate) CertificationRequest);
|
||||
|
||||
impl From<CertificationRequest> for Csr {
|
||||
fn from(certification_request: CertificationRequest) -> Self {
|
||||
Self(certification_request)
|
||||
}
|
||||
}
|
||||
|
||||
impl Csr {
|
||||
pub fn from_der<T: ?Sized + AsRef<[u8]>>(der: &T) -> Result<Self, CsrError> {
|
||||
Ok(from_der(der, ELEMENT_NAME).map(Self)?)
|
||||
}
|
||||
|
||||
pub fn from_pem_str(pem_str: &str) -> Result<Self, CsrError> {
|
||||
Ok(from_pem_str(pem_str, &[CSR_PEM_LABEL], ELEMENT_NAME).map(Self)?)
|
||||
}
|
||||
|
||||
pub fn from_pem(pem: &Pem) -> Result<Self, CsrError> {
|
||||
Ok(from_pem(pem, &[CSR_PEM_LABEL], ELEMENT_NAME).map(Self)?)
|
||||
}
|
||||
|
||||
pub fn to_der(&self) -> Result<Vec<u8>, CsrError> {
|
||||
Ok(to_der(&self.0, ELEMENT_NAME)?)
|
||||
}
|
||||
|
||||
pub fn to_pem(&self) -> Result<Pem<'static>, CsrError> {
|
||||
Ok(to_pem(&self.0, CSR_PEM_LABEL, ELEMENT_NAME)?)
|
||||
}
|
||||
|
||||
pub fn generate(
|
||||
subject: DirectoryName,
|
||||
private_key: &PrivateKey,
|
||||
signature_hash_type: SignatureAlgorithm,
|
||||
) -> Result<Self, CsrError> {
|
||||
let public_key = private_key
|
||||
.to_public_key()
|
||||
.map_err(|source| CsrError::PrivateKeyToPublicKey { source })?;
|
||||
|
||||
let cri = CertificationRequestInfo::new(subject.into(), public_key.into());
|
||||
h_generate_from_cri(cri, private_key, signature_hash_type)
|
||||
}
|
||||
|
||||
pub fn generate_with_attributes(
|
||||
subject: DirectoryName,
|
||||
private_key: &PrivateKey,
|
||||
signature_hash_type: SignatureAlgorithm,
|
||||
attributes: Vec<Attribute>,
|
||||
) -> Result<Self, CsrError> {
|
||||
let public_key = private_key
|
||||
.to_public_key()
|
||||
.map_err(|source| CsrError::PrivateKeyToPublicKey { source })?;
|
||||
|
||||
let mut cri = CertificationRequestInfo::new(subject.into(), public_key.into());
|
||||
for attr in attributes {
|
||||
cri.add_attribute(attr);
|
||||
}
|
||||
h_generate_from_cri(cri, private_key, signature_hash_type)
|
||||
}
|
||||
|
||||
pub fn subject_name(&self) -> DirectoryName {
|
||||
self.0.certification_request_info.subject.clone().into()
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> &PublicKey {
|
||||
(&self.0.certification_request_info.subject_public_key_info).into()
|
||||
}
|
||||
|
||||
pub fn into_subject_infos(self) -> (DirectoryName, PublicKey) {
|
||||
(
|
||||
self.0.certification_request_info.subject.into(),
|
||||
self.0.certification_request_info.subject_public_key_info.into(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn verify(&self) -> Result<(), CsrError> {
|
||||
let hash_type = SignatureAlgorithm::from_algorithm_identifier(&self.0.signature_algorithm)
|
||||
.map_err(|e| CsrError::Signature { source: e })?;
|
||||
|
||||
let public_key = &self.0.certification_request_info.subject_public_key_info;
|
||||
|
||||
let msg =
|
||||
picky_asn1_der::to_vec(&self.0.certification_request_info).map_err(|e| CsrError::Asn1Serialization {
|
||||
source: e,
|
||||
element: "certification request info",
|
||||
})?;
|
||||
|
||||
hash_type
|
||||
.verify(&public_key.clone().into(), &msg, self.0.signature.0.payload_view())
|
||||
.map_err(|e| CsrError::Signature { source: e })?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn h_generate_from_cri(
|
||||
cri: CertificationRequestInfo,
|
||||
private_key: &PrivateKey,
|
||||
signature_hash_type: SignatureAlgorithm,
|
||||
) -> Result<Csr, CsrError> {
|
||||
let cri_der = picky_asn1_der::to_vec(&cri).map_err(|e| CsrError::Asn1Serialization {
|
||||
source: e,
|
||||
element: "certification request cri",
|
||||
})?;
|
||||
let signature = BitString::with_bytes(
|
||||
signature_hash_type
|
||||
.sign(&cri_der, private_key)
|
||||
.map_err(|e| CsrError::Signature { source: e })?,
|
||||
);
|
||||
|
||||
let signature_algorithm = signature_hash_type
|
||||
.try_into()
|
||||
.map_err(|e| CsrError::Signature { source: e })?;
|
||||
|
||||
Ok(Csr(CertificationRequest {
|
||||
certification_request_info: cri,
|
||||
signature_algorithm,
|
||||
signature: signature.into(),
|
||||
}))
|
||||
}
|
||||
Vendored
+182
@@ -0,0 +1,182 @@
|
||||
use picky_asn1::date::{Date, GeneralizedTime, UTCTime, UTCTimeRepr};
|
||||
use picky_asn1_x509::validity::Time;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct UtcDate(GeneralizedTime);
|
||||
|
||||
impl UtcDate {
|
||||
#[inline]
|
||||
pub fn new(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Option<Self> {
|
||||
Some(Self(GeneralizedTime::new(year, month, day, hour, minute, second)?))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn ymd(year: u16, month: u8, day: u8) -> Option<Self> {
|
||||
Some(Self(GeneralizedTime::new(year, month, day, 0, 0, 0)?))
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "time_conversion", feature = "chrono_conversion"))]
|
||||
#[inline]
|
||||
pub fn now() -> Self {
|
||||
#[cfg(feature = "time_conversion")]
|
||||
{
|
||||
Self(time::OffsetDateTime::now_utc().into())
|
||||
}
|
||||
#[cfg(all(feature = "chrono_conversion", not(feature = "time_conversion")))]
|
||||
{
|
||||
Self(chrono::offset::Utc::now().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn year(&self) -> u16 {
|
||||
self.0.year()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn month(&self) -> u8 {
|
||||
self.0.month()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn day(&self) -> u8 {
|
||||
self.0.day()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn hour(&self) -> u8 {
|
||||
self.0.hour()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn minute(&self) -> u8 {
|
||||
self.0.minute()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn second(&self) -> u8 {
|
||||
self.0.second()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UtcDate> for UTCTime {
|
||||
fn from(date: UtcDate) -> Self {
|
||||
unsafe {
|
||||
UTCTime::new_unchecked(
|
||||
date.0.year(),
|
||||
date.0.month(),
|
||||
date.0.day(),
|
||||
date.0.hour(),
|
||||
date.0.minute(),
|
||||
date.0.second(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UTCTime> for UtcDate {
|
||||
fn from(date: Date<UTCTimeRepr>) -> Self {
|
||||
Self(unsafe {
|
||||
GeneralizedTime::new_unchecked(
|
||||
date.year(),
|
||||
date.month(),
|
||||
date.day(),
|
||||
date.hour(),
|
||||
date.minute(),
|
||||
date.second(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UtcDate> for GeneralizedTime {
|
||||
fn from(date: UtcDate) -> GeneralizedTime {
|
||||
date.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GeneralizedTime> for UtcDate {
|
||||
fn from(date: GeneralizedTime) -> Self {
|
||||
Self(date)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UtcDate> for Time {
|
||||
fn from(date: UtcDate) -> Self {
|
||||
// Time is used to encode validity period.
|
||||
// As per RFC 5280,
|
||||
// > CAs conforming to this profile MUST always encode certificate
|
||||
// > validity dates through the year 2049 as UTCTime; certificate validity
|
||||
// > dates in 2050 or later MUST be encoded as GeneralizedTime.
|
||||
// > Conforming applications MUST be able to process validity dates that
|
||||
// > are encoded in either UTCTime or GeneralizedTime.
|
||||
if date.year() >= 2050 {
|
||||
Self::Generalized(Into::<GeneralizedTime>::into(date).into())
|
||||
} else {
|
||||
Self::Utc(Into::<UTCTime>::into(date).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Time> for UtcDate {
|
||||
fn from(time: Time) -> Self {
|
||||
match time {
|
||||
Time::Utc(utc) => utc.0.into(),
|
||||
Time::Generalized(gen_time) => gen_time.0.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for UtcDate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
|
||||
self.year(),
|
||||
self.month(),
|
||||
self.day(),
|
||||
self.hour(),
|
||||
self.minute(),
|
||||
self.second()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "time_conversion")]
|
||||
mod time_convert {
|
||||
use super::*;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
impl From<OffsetDateTime> for UtcDate {
|
||||
fn from(dt: OffsetDateTime) -> Self {
|
||||
Self(dt.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<UtcDate> for OffsetDateTime {
|
||||
type Error = time::error::ComponentRange;
|
||||
|
||||
fn try_from(date: UtcDate) -> Result<Self, Self::Error> {
|
||||
date.0.try_into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "chrono_conversion")]
|
||||
mod chrono_convert {
|
||||
use super::*;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
impl From<DateTime<Utc>> for UtcDate {
|
||||
fn from(dt: DateTime<Utc>) -> Self {
|
||||
Self(dt.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UtcDate> for DateTime<Utc> {
|
||||
fn from(date: UtcDate) -> Self {
|
||||
date.0.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
use crate::hash::HashAlgorithm;
|
||||
use crate::key::{KeyError, PublicKey};
|
||||
use picky_asn1::wrapper::BitStringAsn1Container;
|
||||
use picky_asn1_der::Asn1DerError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum KeyIdGenError {
|
||||
/// asn1 serialization error
|
||||
#[error("(asn1) couldn't serialize {element}: {source}")]
|
||||
Asn1Serialization {
|
||||
element: &'static str,
|
||||
source: Asn1DerError,
|
||||
},
|
||||
|
||||
/// invalid key
|
||||
#[error("invalid key: {source}")]
|
||||
InvalidKey { source: KeyError },
|
||||
}
|
||||
|
||||
/// Describes which method to use to generate key identifiers.
|
||||
///
|
||||
/// See [RFC5280 #4](https://tools.ietf.org/html/rfc5280#section-4.2.1.2) and
|
||||
/// [RFC7093 #2](https://tools.ietf.org/html/rfc7093#section-2).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum KeyIdGenMethod {
|
||||
/// Hash the leftmost 160-bits of the
|
||||
/// SHA-256 hash of the value of the BIT STRING subjectPublicKey
|
||||
/// (excluding the tag, length, and number of unused bits).
|
||||
SPKValueHashedLeftmost160(HashAlgorithm),
|
||||
/// Hash the DER encoding of the SubjectPublicKeyInfo value.
|
||||
SPKFullDER(HashAlgorithm),
|
||||
}
|
||||
|
||||
impl KeyIdGenMethod {
|
||||
pub fn generate_from(self, public_key: &PublicKey) -> Result<Vec<u8>, KeyIdGenError> {
|
||||
use picky_asn1_x509::PublicKey as InnerPublicKey;
|
||||
|
||||
match self {
|
||||
KeyIdGenMethod::SPKValueHashedLeftmost160(hash_algo) => match &public_key.as_inner().subject_public_key {
|
||||
InnerPublicKey::Rsa(BitStringAsn1Container(rsa_pk)) => {
|
||||
let der = picky_asn1_der::to_vec(rsa_pk).map_err(|e| KeyIdGenError::Asn1Serialization {
|
||||
source: e,
|
||||
element: "RSA private key",
|
||||
})?;
|
||||
Ok(hash_algo.digest(&der)[..20].to_vec())
|
||||
}
|
||||
InnerPublicKey::Ec(bitstring) => {
|
||||
let der = bitstring.0.payload_view();
|
||||
Ok(hash_algo.digest(der)[..20].to_vec())
|
||||
}
|
||||
InnerPublicKey::Ed(bitstring) => {
|
||||
let der = bitstring.0.payload_view();
|
||||
Ok(hash_algo.digest(der)[..20].to_vec())
|
||||
}
|
||||
InnerPublicKey::Mldsa(bitstring) => {
|
||||
let der = bitstring.0.payload_view();
|
||||
Ok(hash_algo.digest(der)[..20].to_vec())
|
||||
}
|
||||
},
|
||||
KeyIdGenMethod::SPKFullDER(hash_algo) => {
|
||||
let der = public_key
|
||||
.to_der()
|
||||
.map_err(|e| KeyIdGenError::InvalidKey { source: e })?;
|
||||
Ok(hash_algo.digest(&der))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+151
@@ -0,0 +1,151 @@
|
||||
//! X.509 certificates implementation based on [RFC5280](https://tools.ietf.org/html/rfc5280)
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use picky::key::PrivateKey;
|
||||
//! use picky::pem::{parse_pem, Pem};
|
||||
//! use picky::{signature::SignatureAlgorithm, hash::HashAlgorithm};
|
||||
//! use picky::x509::key_id_gen_method::{KeyIdGenError, KeyIdGenMethod};
|
||||
//! use picky::x509::{certificate::CertType, csr::Csr};
|
||||
//! use picky::x509::{name::DirectoryName, date::UtcDate};
|
||||
//! use picky::x509::certificate::CertificateBuilder;
|
||||
//!
|
||||
//!# use std::error::Error;
|
||||
//!# fn main() -> Result<(), Box<dyn Error>> {
|
||||
//!# let root_key_pem_str =
|
||||
//!# "-----BEGIN PRIVATE KEY-----\n\
|
||||
//!# MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCzl5R1QzQGznTe\n\
|
||||
//!# Jw1STzQz1ZOrbG58ZWdJJDOrjBB60PO+MgOIxDzn41p4W/OeqmzhxUPyzs9d3iAx\n\
|
||||
//!# RcF8gWkS8QvQXtHQtFlux61oE8GHvK9pqrEERQKjjTN/yculekauopAMQOcGDS1L\n\
|
||||
//!# t55z5oByunc/ZQYKHIZ82zfuw/VyKOdJ31/fXn2Djhnfq19YE8pgBDGxEPqCRv7S\n\
|
||||
//!# t/ThptUD+Xr3jwR2ycemq/OVLmsTETh8fYeGrcDIx2EsjL18ptPOg6rZly/UNN+w\n\
|
||||
//!# Sy8L5HlJqzDj03ytYTkgpT442TK3eGpxwX5uaHF7GuL6l9xOIDH+FuPKMt1huFWC\n\
|
||||
//!# Y+hHC8yXAgMBAAECggEATpCbM4lIs0mSOrUe0MkxbxOZ3D49P1IlEwwxRnSpqPDM\n\
|
||||
//!# EIJUpXojotYE9+5R7zFjHIltpkTu1oOb1bJMaKk8TDsDDg7M/t5G2X2k0IkD+tuC\n\
|
||||
//!# aVlwwwtlkiLtSpP+8M9DKJaZKaMi7bTKAfu757wIqSgFjsglEjtUYj6Mg3laYKvk\n\
|
||||
//!# EoR6NsOojR1tOhqGfPFJTH21ZBWDmFHhsyRHXyZOQ9I2u8pv5l33yasgmuZLZ2g3\n\
|
||||
//!# Hok6j0uU/fPpjSRTu6khVZV8+stjnFaHezTWGsz/CLqzC1EWJ1icFTb7UwafyWts\n\
|
||||
//!# EHgdMjqpu2qfqB+miFElhld4fdZIoBWTQwCdN7MnUQKBgQDnBIK6RUJYUjcX9Nei\n\
|
||||
//!# Qvf7Kl3sEY+QTQdI4na3puXjOk6eFqTUAbdsKVwU/yJyR3L+bQNTUmy3GdPlZkYt\n\
|
||||
//!# mx18fGpq9t8whDIFZG9jI5YX4/vEMOHa9zY9FI3QLpIgbifIBEIR76UVe/xakT+I\n\
|
||||
//!# bx8C1p/9msDuqVBJm7pZLIX3DQKBgQDHA2ftRkQVL0lOmvwFl56n2gbnnk0KAZ/8\n\
|
||||
//!# LyuN9FvadqzbpXQqfEUS5f1cfM4JSAXj2e9POBw2FWD1uFROIGPrPEAfSqInI6G9\n\
|
||||
//!# i/MSeBwlcvTXmWMphVGCnJwmCPlznT0Vizr5wIBImgUOpXZpxAqUQUvxOl4R4VYI\n\
|
||||
//!# gLW8iFSpMwKBgCAzacFrC/9hnlpRf4kXipdQ5XyVSgyUIBLRtjiNI/gTVYgFof4H\n\
|
||||
//!# KzkBXttyYKvLN8UtDsybbZnsGLQeGGQc/fQvJ2o3dQ6/LnW/9SK9gBteZOaI5cJu\n\
|
||||
//!# uPm0lrvQ8f9hO1xO86KqY7ll6dv56QAsdQchQXXJD2F06kMIWOY7JYU1AoGAJSKg\n\
|
||||
//!# kOjsqVtSfYV0A4MgSsfnQ+8JBxX4iXEv2mQ/g4tjg/TisU7RAM7DsS9krtyupK60\n\
|
||||
//!# 9f5NXVYt6owDxzRKEMoEWRJvIYiHlLv5lnetINvLjjOECmpjJFEe3gYMriMoE84Q\n\
|
||||
//!# Kixeg62hxfLgHqpDIxjwF8pBZWq7yAhkYRK7YXsCgYAxqdZwpOYyntEBIQa3mOE8\n\
|
||||
//!# a2mthICUXKE30OkMJdrbP3qlFECWZcvKspPHzXWiy8cP9dS2D06ax/UBZmKGXUFv\n\
|
||||
//!# D48nECaQ2glgMPLyyd+tznXed9nZVYBlOFjUQ0fvfpsmhp975n1b/UxAg1jqvigK\n\
|
||||
//!# YHWKXfDNtZ9Ci6BMYVELsw==\n\
|
||||
//!# -----END PRIVATE KEY-----";
|
||||
//!# let intermediate_key_pem_str = root_key_pem_str;
|
||||
//!# let leaf_key_pem_str = root_key_pem_str;
|
||||
//!#
|
||||
//! // Generate a self-signed root certificate
|
||||
//!
|
||||
//! // Load private key
|
||||
//! let root_key = PrivateKey::from_pem_str(root_key_pem_str)?;
|
||||
//!
|
||||
//! let root = CertificateBuilder::new()
|
||||
//! .validity(UtcDate::ymd(2020, 9, 28).unwrap(), UtcDate::ymd(2023, 9, 28).unwrap())
|
||||
//! .self_signed(DirectoryName::new_common_name("My Root CA"), &root_key)
|
||||
//! .ca(true)
|
||||
//! .signature_hash_type(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512))
|
||||
//! .key_id_gen_method(KeyIdGenMethod::SPKFullDER(HashAlgorithm::SHA2_384))
|
||||
//! .build()?;
|
||||
//!
|
||||
//! assert_eq!(root.ty(), CertType::Root);
|
||||
//!
|
||||
//! // Generate intermediate certificate signed by root CA
|
||||
//!
|
||||
//! let intermediate_key = PrivateKey::from_pem_str(intermediate_key_pem_str)?;
|
||||
//!
|
||||
//! let intermediate = CertificateBuilder::new()
|
||||
//! .validity(UtcDate::ymd(2020, 10, 15).unwrap(), UtcDate::ymd(2021, 10, 15).unwrap())
|
||||
//! .subject(DirectoryName::new_common_name("My Authority"), intermediate_key.to_public_key().unwrap())
|
||||
//! .issuer_cert(&root, &root_key)
|
||||
//! .signature_hash_type(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224))
|
||||
//! .key_id_gen_method(KeyIdGenMethod::SPKValueHashedLeftmost160(HashAlgorithm::SHA1))
|
||||
//! .ca(true)
|
||||
//! .pathlen(0)
|
||||
//! .build()?;
|
||||
//!
|
||||
//! assert_eq!(intermediate.ty(), CertType::Intermediate);
|
||||
//!
|
||||
//! // Generate leaf certificate signed by intermediate authority
|
||||
//!
|
||||
//! let leaf_key = PrivateKey::from_pem_str(leaf_key_pem_str)?;
|
||||
//!
|
||||
//! // A CSR can be used
|
||||
//! let csr = Csr::generate(
|
||||
//! DirectoryName::new_common_name("My Leaf"),
|
||||
//! &leaf_key,
|
||||
//! SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1),
|
||||
//! )?;
|
||||
//!
|
||||
//! let signed_leaf = CertificateBuilder::new()
|
||||
//! .validity(UtcDate::ymd(2020, 11, 1).unwrap(), UtcDate::ymd(2021, 1, 1).unwrap())
|
||||
//! .subject_from_csr(csr)
|
||||
//! .issuer_cert(&intermediate, &intermediate_key)
|
||||
//! .signature_hash_type(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384))
|
||||
//! .key_id_gen_method(KeyIdGenMethod::SPKFullDER(HashAlgorithm::SHA2_512))
|
||||
//! .build()?;
|
||||
//!
|
||||
//! assert_eq!(signed_leaf.ty(), CertType::Leaf);
|
||||
//!
|
||||
//! // Check leaf using CA chain
|
||||
//!
|
||||
//! let chain = [intermediate, root];
|
||||
//!
|
||||
//! signed_leaf
|
||||
//! .verifier()
|
||||
//! .chain(chain.iter())
|
||||
//! .exact_date(&UtcDate::ymd(2020, 12, 20).unwrap())
|
||||
//! .verify()?;
|
||||
//!
|
||||
//! // If `not_after` date is behind…
|
||||
//!
|
||||
//! let err = signed_leaf
|
||||
//! .verifier()
|
||||
//! .chain(chain.iter())
|
||||
//! .exact_date(&UtcDate::ymd(2021, 1, 2).unwrap())
|
||||
//! .verify()
|
||||
//! .err()
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! err.to_string(),
|
||||
//! "invalid certificate \'CN=My Leaf\': \
|
||||
//! certificate expired (not after: 2021-01-01 00:00:00, now: 2021-01-02 00:00:00)"
|
||||
//! );
|
||||
//!#
|
||||
//!# Ok(())
|
||||
//!# }
|
||||
//! ```
|
||||
|
||||
#[cfg(feature = "pkcs7")]
|
||||
pub mod pkcs7;
|
||||
|
||||
#[cfg(feature = "wincert")]
|
||||
pub mod wincert;
|
||||
|
||||
pub mod certificate;
|
||||
pub mod csr;
|
||||
pub mod date;
|
||||
pub mod key_id_gen_method;
|
||||
pub mod name;
|
||||
|
||||
pub use certificate::Cert;
|
||||
pub use csr::Csr;
|
||||
pub use key_id_gen_method::KeyIdGenMethod;
|
||||
pub use picky_asn1_x509::{DirectoryString, Extension, Extensions};
|
||||
|
||||
pub mod extension {
|
||||
pub use picky_asn1_x509::extension::*;
|
||||
}
|
||||
|
||||
mod utils;
|
||||
Vendored
+313
@@ -0,0 +1,313 @@
|
||||
use crate::oid::ObjectIdentifier;
|
||||
use picky_asn1::restricted_string::{CharSetError, Ia5String};
|
||||
use picky_asn1::wrapper::{Asn1SequenceOf, Ia5StringAsn1};
|
||||
use picky_asn1_x509::{
|
||||
DirectoryString, GeneralName as SerdeGeneralName, GeneralNames as SerdeGeneralNames, Name, NamePrettyFormatter,
|
||||
OtherName,
|
||||
};
|
||||
use std::fmt;
|
||||
|
||||
// === DirectoryName ===
|
||||
|
||||
pub use picky_asn1_x509::NameAttr;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DirectoryName(Name);
|
||||
|
||||
impl Default for DirectoryName {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectoryName {
|
||||
pub fn new() -> Self {
|
||||
Self(Name::new())
|
||||
}
|
||||
|
||||
pub fn new_common_name<S: Into<DirectoryString>>(name: S) -> Self {
|
||||
Self(Name::new_common_name(name))
|
||||
}
|
||||
|
||||
/// Find the first common name contained in this `Name`
|
||||
pub fn find_common_name(&self) -> Option<&DirectoryString> {
|
||||
self.0.find_common_name()
|
||||
}
|
||||
|
||||
pub fn add_attr<S: Into<DirectoryString>>(&mut self, attr: NameAttr, value: S) {
|
||||
self.0.add_attr(attr, value)
|
||||
}
|
||||
|
||||
/// Add an emailAddress attribute.
|
||||
/// NOTE: this attribute does not conform with the RFC 5280, email should be placed in SAN instead
|
||||
pub fn add_email<S: Into<Ia5StringAsn1>>(&mut self, value: S) {
|
||||
self.0.add_email(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DirectoryName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
NamePrettyFormatter(&self.0).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Name> for DirectoryName {
|
||||
fn from(name: Name) -> Self {
|
||||
Self(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DirectoryName> for Name {
|
||||
fn from(name: DirectoryName) -> Self {
|
||||
name.0
|
||||
}
|
||||
}
|
||||
|
||||
// === GeneralNames === //
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum GeneralName {
|
||||
OtherName(OtherName),
|
||||
RFC822Name(Ia5String),
|
||||
DNSName(Ia5String),
|
||||
DirectoryName(DirectoryName),
|
||||
EDIPartyName {
|
||||
name_assigner: Option<DirectoryString>,
|
||||
party_name: DirectoryString,
|
||||
},
|
||||
URI(Ia5String),
|
||||
IpAddress(Vec<u8>),
|
||||
RegisteredId(ObjectIdentifier),
|
||||
}
|
||||
|
||||
impl GeneralName {
|
||||
pub fn new_rfc822_name<S: Into<String>>(name: S) -> Result<Self, CharSetError> {
|
||||
Ok(Self::RFC822Name(Ia5String::from_string(name.into())?))
|
||||
}
|
||||
|
||||
pub fn new_dns_name<S: Into<String>>(name: S) -> Result<Self, CharSetError> {
|
||||
Ok(Self::DNSName(Ia5String::from_string(name.into())?))
|
||||
}
|
||||
|
||||
pub fn new_directory_name<N: Into<DirectoryName>>(name: N) -> Self {
|
||||
Self::DirectoryName(name.into())
|
||||
}
|
||||
|
||||
pub fn new_edi_party_name<PN, NA>(party_name: PN, name_assigner: Option<NA>) -> Self
|
||||
where
|
||||
PN: Into<DirectoryString>,
|
||||
NA: Into<DirectoryString>,
|
||||
{
|
||||
Self::EDIPartyName {
|
||||
name_assigner: name_assigner.map(Into::into),
|
||||
party_name: party_name.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_uri<S: Into<String>>(uri: S) -> Result<Self, CharSetError> {
|
||||
Ok(Self::URI(Ia5String::from_string(uri.into())?))
|
||||
}
|
||||
|
||||
pub fn new_ip_address<ADDR: Into<Vec<u8>>>(ip_address: ADDR) -> Self {
|
||||
Self::IpAddress(ip_address.into())
|
||||
}
|
||||
|
||||
pub fn new_registered_id<OID: Into<ObjectIdentifier>>(oid: OID) -> Self {
|
||||
Self::RegisteredId(oid.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SerdeGeneralName> for GeneralName {
|
||||
fn from(gn: SerdeGeneralName) -> Self {
|
||||
match gn {
|
||||
SerdeGeneralName::OtherName(other_name) => Self::OtherName(other_name),
|
||||
SerdeGeneralName::Rfc822Name(name) => Self::RFC822Name(name.0),
|
||||
SerdeGeneralName::DnsName(name) => Self::DNSName(name.0),
|
||||
SerdeGeneralName::DirectoryName(name) => Self::DirectoryName(name.into()),
|
||||
SerdeGeneralName::EdiPartyName(edi_pn) => Self::EDIPartyName {
|
||||
name_assigner: edi_pn.name_assigner.0.map(|na| na.0),
|
||||
party_name: edi_pn.party_name.0,
|
||||
},
|
||||
SerdeGeneralName::Uri(uri) => Self::URI(uri.0),
|
||||
SerdeGeneralName::IpAddress(ip_addr) => Self::IpAddress(ip_addr.0),
|
||||
SerdeGeneralName::RegisteredId(id) => Self::RegisteredId(id.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GeneralName> for SerdeGeneralName {
|
||||
fn from(gn: GeneralName) -> Self {
|
||||
match gn {
|
||||
GeneralName::OtherName(other_name) => SerdeGeneralName::OtherName(other_name),
|
||||
GeneralName::RFC822Name(name) => SerdeGeneralName::Rfc822Name(name.into()),
|
||||
GeneralName::DNSName(name) => SerdeGeneralName::DnsName(name.into()),
|
||||
GeneralName::DirectoryName(name) => SerdeGeneralName::DirectoryName(name.into()),
|
||||
GeneralName::EDIPartyName {
|
||||
name_assigner,
|
||||
party_name,
|
||||
} => SerdeGeneralName::new_edi_party_name(party_name, name_assigner),
|
||||
GeneralName::URI(uri) => SerdeGeneralName::Uri(uri.into()),
|
||||
GeneralName::IpAddress(ip_addr) => SerdeGeneralName::IpAddress(ip_addr.into()),
|
||||
GeneralName::RegisteredId(id) => SerdeGeneralName::RegisteredId(id.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GeneralName> for SerdeGeneralNames {
|
||||
fn from(gn: GeneralName) -> Self {
|
||||
GeneralNames::new(gn).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps x509 `GeneralNames` into an easy to use API.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use picky::x509::name::{GeneralNames, GeneralName, DirectoryName};
|
||||
///
|
||||
/// let common_name = GeneralName::new_directory_name(DirectoryName::new_common_name("MyName"));
|
||||
/// let dns_name = GeneralName::new_dns_name("localhost").expect("invalid name string");
|
||||
/// let names = GeneralNames::from(vec![common_name, dns_name]);
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct GeneralNames(SerdeGeneralNames);
|
||||
|
||||
impl GeneralNames {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use picky::x509::name::{GeneralName, GeneralNames};
|
||||
///
|
||||
/// let dns_name = GeneralName::new_dns_name("localhost").expect("invalid name string");
|
||||
/// let names = GeneralNames::new(dns_name);
|
||||
/// ```
|
||||
pub fn new<Gn: Into<GeneralName>>(gn: Gn) -> Self {
|
||||
let gn = gn.into();
|
||||
Self(Asn1SequenceOf(vec![gn.into()]))
|
||||
}
|
||||
|
||||
pub fn new_directory_name<Dn: Into<DirectoryName>>(name: Dn) -> Self {
|
||||
let gn = GeneralName::new_directory_name(name);
|
||||
Self::new(gn)
|
||||
}
|
||||
|
||||
pub fn with_directory_name<Dn: Into<DirectoryName>>(mut self, name: Dn) -> Self {
|
||||
let gn = GeneralName::new_directory_name(name);
|
||||
(self.0).0.push(gn.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn find_directory_name(&self) -> Option<DirectoryName> {
|
||||
for name in &(self.0).0 {
|
||||
if let SerdeGeneralName::DirectoryName(name) = name {
|
||||
return Some(name.clone().into());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use picky::x509::name::GeneralNames;
|
||||
/// use picky_asn1::restricted_string::Ia5String;
|
||||
///
|
||||
/// let names = GeneralNames::new_dns_name(Ia5String::new("localhost").unwrap());
|
||||
/// ```
|
||||
pub fn new_dns_name<Ia5: Into<Ia5String>>(dns_name: Ia5) -> Self {
|
||||
let gn = GeneralName::DNSName(dns_name.into());
|
||||
Self::new(gn)
|
||||
}
|
||||
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use picky::x509::name::{GeneralNames, DirectoryName};
|
||||
/// use picky_asn1::restricted_string::Ia5String;
|
||||
///
|
||||
/// let names = GeneralNames::new_directory_name(DirectoryName::new_common_name("MyName"))
|
||||
/// .with_dns_name(Ia5String::new("localhost").unwrap());
|
||||
/// ```
|
||||
pub fn with_dns_name<Ia5: Into<Ia5String>>(mut self, dns_name: Ia5) -> Self {
|
||||
let gn = GeneralName::DNSName(dns_name.into());
|
||||
(self.0).0.push(gn.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn find_dns_name(&self) -> Option<&Ia5String> {
|
||||
for name in &(self.0).0 {
|
||||
if let SerdeGeneralName::DnsName(name) = name {
|
||||
return Some(&name.0);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn add_name<Gn: Into<GeneralName>>(&mut self, name: Gn) {
|
||||
let gn = name.into();
|
||||
(self.0).0.push(gn.into());
|
||||
}
|
||||
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use picky::x509::name::{GeneralNames, GeneralName, DirectoryName};
|
||||
///
|
||||
/// let common_name = GeneralName::new_directory_name(DirectoryName::new_common_name("MyName"));
|
||||
/// let dns_name = GeneralName::new_dns_name("localhost").expect("invalid name string");
|
||||
/// let names = GeneralNames::new(common_name).with_name(dns_name);
|
||||
/// ```
|
||||
pub fn with_name<GN: Into<GeneralName>>(mut self, name: GN) -> Self {
|
||||
let gn = name.into();
|
||||
(self.0).0.push(gn.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn into_general_names(self) -> Vec<GeneralName> {
|
||||
(self.0).0.into_iter().map(|gn| gn.into()).collect()
|
||||
}
|
||||
|
||||
pub fn to_general_names(&self) -> Vec<GeneralName> {
|
||||
(self.0).0.iter().map(|gn| gn.clone().into()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SerdeGeneralNames> for GeneralNames {
|
||||
fn from(gn: SerdeGeneralNames) -> Self {
|
||||
Self(gn)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GeneralNames> for SerdeGeneralNames {
|
||||
fn from(gn: GeneralNames) -> Self {
|
||||
gn.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<GeneralName>> for GeneralNames {
|
||||
fn from(names: Vec<GeneralName>) -> Self {
|
||||
let serde_names = names.into_iter().map(|n| n.into()).collect();
|
||||
Self(Asn1SequenceOf(serde_names))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn build_and_format_directory_name() {
|
||||
let mut my_name = DirectoryName::new_common_name("CommonName");
|
||||
my_name.add_attr(NameAttr::StateOrProvinceName, "SomeState");
|
||||
my_name.add_attr(NameAttr::CountryName, "SomeCountry");
|
||||
assert_eq!(my_name.to_string(), "CN=CommonName,ST=SomeState,C=SomeCountry");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_common_name() {
|
||||
let my_name = DirectoryName::new_common_name("CommonName");
|
||||
let cn = my_name.find_common_name().unwrap();
|
||||
assert_eq!(cn.to_utf8_lossy(), "CommonName");
|
||||
}
|
||||
}
|
||||
+2558
File diff suppressed because it is too large
Load Diff
+111
@@ -0,0 +1,111 @@
|
||||
use crate::x509::pkcs7::{Pkcs7, Pkcs7Error};
|
||||
use picky_asn1_x509::content_info::ContentValue;
|
||||
use picky_asn1_x509::pkcs7::ctl::CTLEntry;
|
||||
pub use picky_asn1_x509::pkcs7::ctl::CTLEntryAttributeValues;
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CtlError {
|
||||
#[error("Failed to download CTL: {description}")]
|
||||
DownloadError { description: String },
|
||||
#[error("{description}")]
|
||||
ExtractingError { description: String },
|
||||
#[error("Failed to parse CertificateTrustList: {0}")]
|
||||
FailedToParseCtl(Pkcs7Error),
|
||||
#[error(transparent)]
|
||||
IoError(#[from] io::Error),
|
||||
#[error("For CTL we expect CertificateTrustList content in EncapsulatedContentInfo, but something else")]
|
||||
IncorrectContentValue,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CertificateTrustList {
|
||||
pkcs7: Pkcs7,
|
||||
}
|
||||
|
||||
impl CertificateTrustList {
|
||||
pub fn ctl_entries(&self) -> Result<&[CTLEntry], CtlError> {
|
||||
let content_value = self
|
||||
.pkcs7
|
||||
.0
|
||||
.signed_data
|
||||
.content_info
|
||||
.content
|
||||
.as_ref()
|
||||
.expect("CTL Content should be present in EncapsulatedContentInfo");
|
||||
|
||||
let ctl = match &content_value.0 {
|
||||
ContentValue::CertificateTrustList(ctl) => ctl,
|
||||
_ => return Err(CtlError::IncorrectContentValue),
|
||||
};
|
||||
|
||||
Ok(&ctl.crl_entries.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ctl_http_fetch")]
|
||||
pub mod http_fetch {
|
||||
use super::*;
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
pub trait CtlHttpFetch {
|
||||
fn fetch() -> Result<CertificateTrustList, CtlError>;
|
||||
}
|
||||
|
||||
impl CtlHttpFetch for CertificateTrustList {
|
||||
fn fetch() -> Result<CertificateTrustList, CtlError> {
|
||||
let ctl_url: &str =
|
||||
"http://www.download.windowsupdate.com/msdownload/update/v3/static/trustedr/en/authrootstl.cab";
|
||||
|
||||
let mut cab = reqwest::blocking::get(ctl_url).map_err(|err| CtlError::DownloadError {
|
||||
description: err.to_string(),
|
||||
})?;
|
||||
|
||||
if !cab.status().is_success() {
|
||||
return Err(CtlError::DownloadError {
|
||||
description: format!("Response status code is {}", cab.status()),
|
||||
});
|
||||
}
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
cab.copy_to(&mut buffer).map_err(|err| CtlError::ExtractingError {
|
||||
description: format!("Failed to copy Response body to Vec: {err}"),
|
||||
})?;
|
||||
|
||||
let mut cabinet = cab::Cabinet::new(Cursor::new(&mut buffer)).map_err(|err| CtlError::ExtractingError {
|
||||
description: format!("Failed to parse Cabinet file: {err}"),
|
||||
})?;
|
||||
|
||||
let mut authroot = cabinet
|
||||
.read_file("authroot.stl")
|
||||
.expect("authroot.stl should be present in authrootstl.cab");
|
||||
|
||||
let mut ctl_buffer = Vec::new();
|
||||
authroot.read_to_end(&mut ctl_buffer)?;
|
||||
|
||||
let pkcs7: Pkcs7 = Pkcs7::from_der(&ctl_buffer).map_err(CtlError::FailedToParseCtl)?;
|
||||
|
||||
Ok(CertificateTrustList { pkcs7 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::x509::pkcs7::Pkcs7;
|
||||
|
||||
#[test]
|
||||
fn parse_certificate_trust_list_in_der() {
|
||||
Pkcs7::from_der(picky_test_data::CERTIFICATE_TRUST_LIST).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(feature = "ctl_http_fetch")]
|
||||
#[ignore = "temporarily disabled for CI issues,which is under investigation"]
|
||||
#[test]
|
||||
fn create_ctl() {
|
||||
use super::http_fetch::CtlHttpFetch;
|
||||
CertificateTrustList::fetch().unwrap();
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
use crate::AlgorithmIdentifier;
|
||||
use crate::pem::Pem;
|
||||
use crate::x509::certificate::{Cert, CertError};
|
||||
use crate::x509::utils::{from_der, from_pem, from_pem_str, to_der, to_pem};
|
||||
use picky_asn1_der::Asn1DerError;
|
||||
use picky_asn1_x509::content_info::EncapsulatedContentInfo;
|
||||
use picky_asn1_x509::pkcs7::Pkcs7Certificate;
|
||||
use picky_asn1_x509::signed_data::CertificateChoices;
|
||||
use picky_asn1_x509::signer_info::SignerInfo;
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod authenticode;
|
||||
#[cfg(feature = "ctl")]
|
||||
pub mod ctl;
|
||||
pub mod timestamp;
|
||||
|
||||
type Pkcs7Result<T> = Result<T, Pkcs7Error>;
|
||||
|
||||
const ELEMENT_NAME: &str = "pkcs7 certificate";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Pkcs7Error {
|
||||
#[error(transparent)]
|
||||
Cert(#[from] CertError),
|
||||
#[error(transparent)]
|
||||
Asn1DerError(#[from] Asn1DerError),
|
||||
}
|
||||
|
||||
const PKCS7_PEM_LABEL: &str = "PKCS7";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Pkcs7(Pkcs7Certificate);
|
||||
|
||||
impl Pkcs7 {
|
||||
pub fn from_der<V: ?Sized + AsRef<[u8]>>(data: &V) -> Pkcs7Result<Self> {
|
||||
Ok(from_der(data, ELEMENT_NAME).map(Self)?)
|
||||
}
|
||||
|
||||
pub fn from_pem(pem: &Pem) -> Pkcs7Result<Self> {
|
||||
Ok(from_pem(pem, &[PKCS7_PEM_LABEL], ELEMENT_NAME).map(Self)?)
|
||||
}
|
||||
|
||||
pub fn from_pem_str(pem_str: &str) -> Pkcs7Result<Self> {
|
||||
Ok(from_pem_str(pem_str, &[PKCS7_PEM_LABEL], ELEMENT_NAME).map(Self)?)
|
||||
}
|
||||
|
||||
pub fn to_der(&self) -> Pkcs7Result<Vec<u8>> {
|
||||
Ok(to_der(&self.0, ELEMENT_NAME)?)
|
||||
}
|
||||
|
||||
pub fn to_pem(&self) -> Pkcs7Result<Pem<'static>> {
|
||||
Ok(to_pem(&self.0, PKCS7_PEM_LABEL, ELEMENT_NAME)?)
|
||||
}
|
||||
|
||||
pub fn digest_algorithms(&self) -> &[AlgorithmIdentifier] {
|
||||
self.0.signed_data.digest_algorithms.0.0.as_slice()
|
||||
}
|
||||
|
||||
pub fn signer_infos(&self) -> &[SignerInfo] {
|
||||
&self.0.signed_data.signers_infos.0
|
||||
}
|
||||
|
||||
pub fn encapsulated_content_info(&self) -> &EncapsulatedContentInfo {
|
||||
&self.0.signed_data.0.content_info
|
||||
}
|
||||
|
||||
pub fn decode_certificates(&self) -> Vec<Cert> {
|
||||
self.0
|
||||
.signed_data
|
||||
.certificates
|
||||
.0
|
||||
.0
|
||||
.iter()
|
||||
.filter_map(|cert| match cert {
|
||||
CertificateChoices::Certificate(certificate) => Cert::from_der(&certificate.0).ok(),
|
||||
CertificateChoices::Other(_) => None,
|
||||
})
|
||||
.collect::<Vec<Cert>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pkcs7Certificate> for Pkcs7 {
|
||||
fn from(pkcs7_certificate: Pkcs7Certificate) -> Self {
|
||||
Pkcs7(pkcs7_certificate)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pkcs7> for Pkcs7Certificate {
|
||||
fn from(pkcs7: Pkcs7) -> Self {
|
||||
pkcs7.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::pem::parse_pem;
|
||||
|
||||
#[test]
|
||||
fn read_pem_and_parse_certificate() {
|
||||
let pem = parse_pem(picky_test_data::PKCS7.as_bytes()).unwrap();
|
||||
Pkcs7::from_pem(&pem).unwrap();
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
use crate::hash::HashAlgorithm;
|
||||
use crate::x509::certificate::CertError;
|
||||
use crate::x509::pkcs7::{Pkcs7, Pkcs7Error};
|
||||
use crate::x509::utils::{from_der, to_der};
|
||||
use picky_asn1::wrapper::ExplicitContextTag0;
|
||||
|
||||
use picky_asn1_x509::oids;
|
||||
use picky_asn1_x509::pkcs7::content_info::{ContentValue, EncapsulatedContentInfo};
|
||||
use picky_asn1_x509::pkcs7::signed_data::SignedData;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TimestampError {
|
||||
#[error(transparent)]
|
||||
Asn1DerError(#[from] CertError),
|
||||
#[error(transparent)]
|
||||
Pkcs7Error(#[from] Pkcs7Error),
|
||||
#[error("Timestamp token is empty")]
|
||||
TimestampTokenEmpty,
|
||||
#[cfg(feature = "http_timestamp")]
|
||||
#[error("Failed to decode base64 response")]
|
||||
Base64DecodeError,
|
||||
#[cfg(feature = "http_timestamp")]
|
||||
#[error("Remote Authenticode TSA server responded with `{0}` status code")]
|
||||
BadResponse(reqwest::StatusCode),
|
||||
#[cfg(feature = "http_timestamp")]
|
||||
#[error("Remote Authenticode TSA server response error: {0}")]
|
||||
RemoteServerResponseError(reqwest::Error),
|
||||
#[cfg(feature = "http_timestamp")]
|
||||
#[error("Badly formatted URL")]
|
||||
BadUrl,
|
||||
}
|
||||
|
||||
pub trait Timestamper: Sized {
|
||||
fn timestamp(&self, digest: Vec<u8>, hash_algo: HashAlgorithm) -> Result<Pkcs7, TimestampError>; // hash_algo is used in RFC3161
|
||||
fn modify_signed_data(&self, token: Pkcs7, signed_data: &mut SignedData);
|
||||
}
|
||||
|
||||
#[cfg(feature = "http_timestamp")]
|
||||
pub mod http_timestamp {
|
||||
use super::*;
|
||||
use base64::engine::{Engine as _, general_purpose};
|
||||
use picky_asn1_x509::pkcs7::signer_info::{UnsignedAttribute, UnsignedAttributeValue};
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::header::{CACHE_CONTROL, CONTENT_LENGTH, CONTENT_TYPE};
|
||||
use reqwest::{Method, StatusCode, Url};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AuthenticodeTimestamper {
|
||||
url: Url,
|
||||
}
|
||||
|
||||
impl AuthenticodeTimestamper {
|
||||
pub fn new<U: AsRef<str>>(url: U) -> Result<AuthenticodeTimestamper, TimestampError> {
|
||||
let url = Url::parse(url.as_ref()).map_err(|_| TimestampError::BadUrl)?;
|
||||
Ok(Self { url })
|
||||
}
|
||||
}
|
||||
|
||||
impl Timestamper for AuthenticodeTimestamper {
|
||||
fn timestamp(&self, digest: Vec<u8>, _: HashAlgorithm) -> Result<Pkcs7, TimestampError> {
|
||||
let timestamp_request = TimestampRequest::new(digest);
|
||||
|
||||
let client = Client::new();
|
||||
let content = general_purpose::STANDARD.encode(timestamp_request.to_der()?);
|
||||
|
||||
let request = client
|
||||
.request(Method::POST, self.url.clone())
|
||||
.header(CACHE_CONTROL, "no-cache")
|
||||
.header(CONTENT_TYPE, "application/octet-stream")
|
||||
.header(CONTENT_LENGTH, content.len())
|
||||
.body(content)
|
||||
.build()
|
||||
.expect("RequestBuilder should not panic");
|
||||
|
||||
let response = client
|
||||
.execute(request)
|
||||
.map_err(TimestampError::RemoteServerResponseError)?;
|
||||
|
||||
if response.status() != StatusCode::OK {
|
||||
return Err(TimestampError::BadResponse(response.status()));
|
||||
}
|
||||
|
||||
let mut body = response
|
||||
.bytes()
|
||||
.map_err(TimestampError::RemoteServerResponseError)?
|
||||
.to_vec();
|
||||
|
||||
body.retain(|&x| x != b'\n' && x != b'\r' && x != b'\0'); // Removing CRLF entries
|
||||
|
||||
let der = general_purpose::STANDARD
|
||||
.decode(body)
|
||||
.map_err(|_| TimestampError::Base64DecodeError)?;
|
||||
let token = Pkcs7::from_der(&der).map_err(TimestampError::Pkcs7Error)?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
fn modify_signed_data(&self, token: Pkcs7, signed_data: &mut SignedData) {
|
||||
let SignedData {
|
||||
certificates,
|
||||
signers_infos,
|
||||
..
|
||||
} = token.0.signed_data.0;
|
||||
|
||||
let singer_info = signers_infos
|
||||
.0
|
||||
.first()
|
||||
.expect("Exactly one SignedInfo should be present");
|
||||
|
||||
let unsigned_attribute = UnsignedAttribute {
|
||||
ty: oids::counter_sign().into(),
|
||||
value: UnsignedAttributeValue::CounterSign(vec![singer_info.clone()].into()),
|
||||
};
|
||||
|
||||
let signer_info = signed_data
|
||||
.signers_infos
|
||||
.0
|
||||
.0
|
||||
.first_mut()
|
||||
.expect("Exactly one SignedInfo should be present");
|
||||
|
||||
signer_info.unsigned_attrs.0.0.push(unsigned_attribute);
|
||||
|
||||
signed_data.certificates.0.0.extend(certificates.0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ELEMENT_NAME: &str = "TimestampRequest";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TimestampRequest(picky_asn1_x509::timestamp::TimestampRequest);
|
||||
|
||||
impl TimestampRequest {
|
||||
pub fn new(digest: Vec<u8>) -> TimestampRequest {
|
||||
Self(picky_asn1_x509::timestamp::TimestampRequest {
|
||||
countersignature_type: oids::timestamp_request().into(),
|
||||
content: EncapsulatedContentInfo {
|
||||
content_type: oids::content_info_type_data().into(),
|
||||
content: Some(ContentValue::Data(digest.into()).into()),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn content(&self) -> &EncapsulatedContentInfo {
|
||||
&self.0.content
|
||||
}
|
||||
|
||||
pub fn into_content(self) -> EncapsulatedContentInfo {
|
||||
self.0.content
|
||||
}
|
||||
|
||||
pub fn digest(&self) -> &[u8] {
|
||||
if let ExplicitContextTag0(ContentValue::Data(data)) = self.0.content.content.as_ref().unwrap() {
|
||||
&data.0
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_der<V: ?Sized + AsRef<[u8]>>(data: &V) -> Result<Self, CertError> {
|
||||
from_der(data, ELEMENT_NAME).map(Self)
|
||||
}
|
||||
|
||||
pub fn to_der(&self) -> Result<Vec<u8>, CertError> {
|
||||
to_der(&self.0, ELEMENT_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TimestampRequest> for picky_asn1_x509::timestamp::TimestampRequest {
|
||||
fn from(tr: TimestampRequest) -> picky_asn1_x509::timestamp::TimestampRequest {
|
||||
tr.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<picky_asn1_x509::timestamp::TimestampRequest> for TimestampRequest {
|
||||
fn from(tr: picky_asn1_x509::timestamp::TimestampRequest) -> TimestampRequest {
|
||||
Self(tr)
|
||||
}
|
||||
}
|
||||
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::certificate::CertError;
|
||||
use crate::pem::{Pem, parse_pem};
|
||||
|
||||
pub(super) fn from_der<'a, T, V>(data: &'a V, element: &'static str) -> Result<T, CertError>
|
||||
where
|
||||
T: Deserialize<'a>,
|
||||
V: ?Sized + AsRef<[u8]>,
|
||||
{
|
||||
picky_asn1_der::from_bytes(data.as_ref()).map_err(|e| CertError::Asn1Deserialization { source: e, element })
|
||||
}
|
||||
|
||||
pub(super) fn from_pem<'a, T: Deserialize<'a>>(
|
||||
pem: &'a Pem,
|
||||
valid_pem_labels: &[&str],
|
||||
element: &'static str,
|
||||
) -> Result<T, CertError> {
|
||||
if valid_pem_labels.iter().any(|&label| pem.label() == label) {
|
||||
from_der(pem.data(), element)
|
||||
} else {
|
||||
Err(CertError::InvalidPemLabel {
|
||||
label: pem.label().to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn from_pem_str<T>(pem_str: &str, valid_pem_labels: &[&str], element: &'static str) -> Result<T, CertError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
let pem = parse_pem(pem_str)?;
|
||||
from_pem(&pem, valid_pem_labels, element)
|
||||
}
|
||||
|
||||
pub(super) fn to_der<T: Serialize>(val: &T, element: &'static str) -> Result<Vec<u8>, CertError> {
|
||||
picky_asn1_der::to_vec(val).map_err(|e| CertError::Asn1Serialization { source: e, element })
|
||||
}
|
||||
|
||||
pub(super) fn to_pem<T: Serialize>(val: &T, pem_label: &str, element: &'static str) -> Result<Pem<'static>, CertError> {
|
||||
Ok(Pem::new(pem_label, to_der(val, element)?))
|
||||
}
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use std::error;
|
||||
use std::io::{self, BufReader, BufWriter, Write};
|
||||
use thiserror::Error;
|
||||
|
||||
const MINIMUM_BYTES_TO_DECODE: usize = 4 /* WinCertificate::length */ + 2 /* WinCertificate::revision */ + 2 /* WinCertificate::certificate */;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum WinCertificateError {
|
||||
#[error("Revision value is wrong(expected any of {expected}, but {got} got)")]
|
||||
WrongRevisionValue { expected: String, got: u16 },
|
||||
#[error("Certificate type is wrong(expected any of {expected}, but {got} got)")]
|
||||
WrongCertificateType { expected: String, got: u16 },
|
||||
#[error("Length is wrong({minimum} at least, but {got} got)")]
|
||||
WrongLength { minimum: usize, got: usize },
|
||||
#[error("Certificate data is empty")]
|
||||
CertificateDataIsEmpty,
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
#[error(transparent)]
|
||||
Other(#[from] Box<dyn error::Error>),
|
||||
}
|
||||
|
||||
pub type WinCertificateResult<T> = Result<T, WinCertificateError>;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct WinCertificate {
|
||||
length: u32,
|
||||
revision: RevisionType,
|
||||
certificate_type: CertificateType,
|
||||
certificate: Vec<u8>,
|
||||
}
|
||||
|
||||
impl WinCertificate {
|
||||
pub fn decode<V: ?Sized + AsRef<[u8]>>(data: &V) -> WinCertificateResult<Self> {
|
||||
if data.as_ref().len() < MINIMUM_BYTES_TO_DECODE {
|
||||
return Err(WinCertificateError::WrongLength {
|
||||
minimum: MINIMUM_BYTES_TO_DECODE,
|
||||
got: data.as_ref().len(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut buffer = BufReader::with_capacity(data.as_ref().len(), data.as_ref());
|
||||
|
||||
let length = buffer.read_u32::<LittleEndian>()?;
|
||||
|
||||
if length == 0 {
|
||||
return Err(WinCertificateError::CertificateDataIsEmpty);
|
||||
}
|
||||
|
||||
let revision = RevisionType::try_from(buffer.read_u16::<LittleEndian>()?)?;
|
||||
|
||||
let certificate_type = CertificateType::try_from(buffer.read_u16::<LittleEndian>()?)?;
|
||||
|
||||
let certificate_length = length as usize - MINIMUM_BYTES_TO_DECODE;
|
||||
let mut certificate = Vec::with_capacity(certificate_length);
|
||||
|
||||
for _ in 0..certificate_length {
|
||||
certificate.push(buffer.read_u8()?);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
length,
|
||||
revision,
|
||||
certificate_type,
|
||||
certificate,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encode(self) -> WinCertificateResult<Vec<u8>> {
|
||||
let Self {
|
||||
length,
|
||||
revision,
|
||||
certificate_type,
|
||||
certificate,
|
||||
} = self;
|
||||
|
||||
let padding = (8 - (certificate.len() % 8)) % 8;
|
||||
|
||||
let mut buffer = BufWriter::with_capacity(length as usize + padding, Vec::new());
|
||||
|
||||
buffer.write_u32::<LittleEndian>(length + padding as u32)?;
|
||||
buffer.write_u16::<LittleEndian>(revision as u16)?;
|
||||
buffer.write_u16::<LittleEndian>(certificate_type as u16)?;
|
||||
|
||||
buffer.write_all(&certificate)?;
|
||||
buffer.write_all(&vec![0; padding])?;
|
||||
|
||||
buffer
|
||||
.into_inner()
|
||||
.map_err(|err| WinCertificateError::Other(Box::new(err) as Box<dyn error::Error>))
|
||||
}
|
||||
|
||||
pub fn from_certificate<V: Into<Vec<u8>>>(certificate: V, certificate_type: CertificateType) -> Self {
|
||||
let certificate = certificate.into();
|
||||
Self {
|
||||
// According to Authenticode_PE.docx WinCertificate::length is set to the WinCertificate::certificate length,
|
||||
// but if the length does not include other WinCertificate's fields (WinCertificate::revision,
|
||||
// WinCertificate::certificate_type and WinCertificate::length itself) sizes the signature will invalid.
|
||||
// MSDN says the same:
|
||||
// dwLength - Specifies the length, in bytes, of the signature
|
||||
// (see https://docs.microsoft.com/en-us/windows/win32/api/wintrust/ns-wintrust-win_certificate).
|
||||
length: (MINIMUM_BYTES_TO_DECODE + certificate.len()) as u32,
|
||||
revision: RevisionType::WinCertificateRevision20,
|
||||
certificate_type,
|
||||
certificate,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_certificate(&self) -> &[u8] {
|
||||
self.certificate.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[repr(u16)]
|
||||
pub enum RevisionType {
|
||||
WinCertificateRevision10 = 0x0100,
|
||||
WinCertificateRevision20 = 0x0200,
|
||||
}
|
||||
|
||||
impl TryFrom<u16> for RevisionType {
|
||||
type Error = WinCertificateError;
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x0100 => Ok(RevisionType::WinCertificateRevision10),
|
||||
0x0200 => Ok(RevisionType::WinCertificateRevision20),
|
||||
_ => Err(WinCertificateError::WrongRevisionValue {
|
||||
expected: format!("{:?}", [0x0100, 0x0200]),
|
||||
got: value,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[repr(u16)]
|
||||
pub enum CertificateType {
|
||||
WinCertTypeX509 = 0x0001,
|
||||
WinCertTypePkcsSignedData = 0x0002,
|
||||
WinCertTypeReserved1 = 0x0003,
|
||||
WinCertTypePkcs1Sign = 0x0009,
|
||||
}
|
||||
|
||||
impl TryFrom<u16> for CertificateType {
|
||||
type Error = WinCertificateError;
|
||||
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x0001 => Ok(CertificateType::WinCertTypeX509),
|
||||
0x0002 => Ok(CertificateType::WinCertTypePkcsSignedData),
|
||||
0x0003 => Ok(CertificateType::WinCertTypeReserved1),
|
||||
0x0009 => Ok(CertificateType::WinCertTypePkcs1Sign),
|
||||
_ => Err(WinCertificateError::WrongCertificateType {
|
||||
expected: format!("{:?}", [0x0001, 0x0002, 0x0003, 0x0009]),
|
||||
got: value,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const WINCERT_WITH_INVALID_LENGTH: [u8; 8] = [
|
||||
0x00, 0x00, 0x00, 0x00, // -> WIN_CERTIFICATE::dwLength = 0x00 = 0
|
||||
0x00, 0x01, // -> WIN_CERTIFICATE::wRevision = 0x0100 = WIN_CERT_REVISION_1_0
|
||||
0x01,
|
||||
0x00, // -> WIN_CERTIFICATE::wCertificateType = 0x01 = WIN_CERTIFICATE::WIN_CERT_TYPE_X509
|
||||
// empty WIN_CERTIFICATE::bCertificate field
|
||||
];
|
||||
|
||||
const WINCERT_WITH_ONE_BYTE_CERTIFICATE: [u8; 16] = [
|
||||
0x10, 0x00, 0x00, 0x00, // -> WIN_CERTIFICATE::dwLength = 0x10 = 16
|
||||
0x00, 0x01, // -> WIN_CERTIFICATE::wRevision = 0x0100 = WIN_CERT_REVISION_1_0
|
||||
0x01, 0x00, // -> WIN_CERTIFICATE::wCertificateType = 0x01 = WIN_CERTIFICATE::WIN_CERT_TYPE_X509
|
||||
0x01, // -> WIN_CERTIFICATE::bCertificate = bCertificate[0] = 1
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // padding
|
||||
];
|
||||
|
||||
const WINCERT_WITH_TEN_BYTES_CERTIFICATE: [u8; 24] = [
|
||||
0x18, 0x00, 0x00, 0x00, // -> WIN_CERTIFICATE::dwLength = 0x18 = 24
|
||||
0x00, 0x02, // -> WIN_CERTIFICATE::wRevision = 0x0200 = WIN_CERT_REVISION_2_0
|
||||
0x09, 0x00, // -> WIN_CERTIFICATE::wCertificateType = 0x09 = WIN_CERTIFICATE::WinCertTypePkcs1Sign
|
||||
0x01, 0x20, 0x03, 0x40, 0x05, 0x60, 0x70, 0x08, // -> WIN_CERTIFICATE::bCertificate = bCertificate[10]
|
||||
0x90, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // padding
|
||||
];
|
||||
|
||||
const WINCERT_WITH_INVALID_REVISION: [u8; 9] = [
|
||||
0x09, 0x00, 0x00, 0x00, // -> WIN_CERTIFICATE::dwLength = 0x09 = 9
|
||||
0x00, 0x03, // -> WIN_CERTIFICATE::wRevision = 0x0300(not existing)
|
||||
0x01, 0x00, // -> WIN_CERTIFICATE::wCertificateType = 0x01 = WIN_CERTIFICATE::WIN_CERT_TYPE_X509
|
||||
0x01, // -> WIN_CERTIFICATE::bCertificate = bCertificate[0] = 1
|
||||
];
|
||||
|
||||
const WINCERT_WITH_X509_CERTIFICATE: [u8; 136] = [
|
||||
0x88, 0x00, 0x00, 0x00, // -> WIN_CERTIFICATE::dwLength = 0x80 = 136
|
||||
0x00, 0x02, // -> WIN_CERTIFICATE::wRevision = 0x0200 = WIN_CERT_REVISION_2_0
|
||||
0x01, 0x00, // -> WIN_CERTIFICATE::wCertificateType = 0x01 = WIN_CERTIFICATE::WIN_CERT_TYPE_X509
|
||||
// X509 certificate
|
||||
0x0B, 0x04, 0x55, 0x03, 0x06, 0x15, 0x30, 0x17, 0x31, 0x74, 0x69, 0x72, 0x6F, 0x69, 0x72, 0x70, 0x41, 0x08,
|
||||
0x0C, 0x0A, 0x04, 0x55, 0x03, 0x06, 0x0F, 0x30, 0x11, 0x31, 0x6F, 0x72, 0x70, 0x69, 0x6E, 0x44, 0x06, 0x0C,
|
||||
0x07, 0x04, 0x55, 0x03, 0x06, 0x0D, 0x30, 0x0F, 0x31, 0x6F, 0x72, 0x70, 0x69, 0x6E, 0x44, 0x06, 0x0C, 0x08,
|
||||
0x04, 0x55, 0x03, 0x06, 0x0D, 0x30, 0x0F, 0x31, 0x41, 0x55, 0x02, 0x13, 0x06, 0x04, 0x55, 0x03, 0x06, 0x09,
|
||||
0x30, 0x0B, 0x31, 0x97, 0x81, 0x30, 0x00, 0x05, 0x0B, 0x01, 0x01, 0x0D, 0xF7, 0x86, 0x48, 0x86, 0x2A, 0x09,
|
||||
0x06, 0x0D, 0x30, 0x92, 0x72, 0xDC, 0xE9, 0x6B, 0x13, 0x8F, 0x0D, 0x06, 0xFB, 0xC1, 0xD1, 0x69, 0x97, 0x79,
|
||||
0x4B, 0xA1, 0x69, 0x63, 0x19, 0x14, 0x02, 0x02, 0x01, 0x02, 0x03, 0xA0, 0xF9, 0x03, 0x82, 0x30, 0x11, 0x06,
|
||||
0x82, 0x30,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn decode_with_invalid_length() {
|
||||
let decoded = WinCertificate::decode(WINCERT_WITH_INVALID_LENGTH.as_ref());
|
||||
assert!(decoded.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_wincert_with_one_byte_certificate() {
|
||||
let decoded = WinCertificate::decode(WINCERT_WITH_ONE_BYTE_CERTIFICATE.as_ref()).unwrap();
|
||||
|
||||
pretty_assertions::assert_eq!(decoded.length, 16);
|
||||
pretty_assertions::assert_eq!(decoded.revision, RevisionType::WinCertificateRevision10);
|
||||
pretty_assertions::assert_eq!(decoded.certificate_type, CertificateType::WinCertTypeX509);
|
||||
pretty_assertions::assert_eq!(decoded.certificate, vec![1, 0, 0, 0, 0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_into_wincert_with_one_byte_certificate() {
|
||||
let wincert = WinCertificate {
|
||||
length: 9,
|
||||
revision: RevisionType::WinCertificateRevision10,
|
||||
certificate_type: CertificateType::WinCertTypeX509,
|
||||
certificate: vec![1],
|
||||
};
|
||||
|
||||
let encoded = wincert.encode().unwrap();
|
||||
assert_eq!(encoded, WINCERT_WITH_ONE_BYTE_CERTIFICATE.to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_wincert_with_ten_bytes_certificate() {
|
||||
let decoded = WinCertificate::decode(WINCERT_WITH_TEN_BYTES_CERTIFICATE.as_ref()).unwrap();
|
||||
|
||||
pretty_assertions::assert_eq!(decoded.length, 24);
|
||||
pretty_assertions::assert_eq!(decoded.revision, RevisionType::WinCertificateRevision20);
|
||||
pretty_assertions::assert_eq!(decoded.certificate_type, CertificateType::WinCertTypePkcs1Sign);
|
||||
pretty_assertions::assert_eq!(
|
||||
decoded.certificate,
|
||||
vec![1, 32, 3, 64, 5, 96, 112, 8, 144, 1, 0, 0, 0, 0, 0, 0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_into_wincert_with_ten_bytes_certificate() {
|
||||
let wincert = WinCertificate {
|
||||
length: 18,
|
||||
revision: RevisionType::WinCertificateRevision20,
|
||||
certificate_type: CertificateType::WinCertTypePkcs1Sign,
|
||||
certificate: vec![1, 32, 3, 64, 5, 96, 112, 8, 144, 1],
|
||||
};
|
||||
|
||||
let encoded = wincert.encode().unwrap();
|
||||
assert_eq!(encoded, WINCERT_WITH_TEN_BYTES_CERTIFICATE.to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_wincert_with_invalid_revision() {
|
||||
let decoded = WinCertificate::decode(WINCERT_WITH_INVALID_REVISION.as_ref());
|
||||
assert!(decoded.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_wincert_with_x509_certificate() {
|
||||
WinCertificate::decode(WINCERT_WITH_X509_CERTIFICATE.as_ref()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_wincert_with_x509_certificate() {
|
||||
let wincert = WinCertificate {
|
||||
length: 136,
|
||||
revision: RevisionType::WinCertificateRevision20,
|
||||
certificate_type: CertificateType::WinCertTypeX509,
|
||||
certificate: WINCERT_WITH_X509_CERTIFICATE[8..].to_vec(),
|
||||
};
|
||||
|
||||
let encoded = wincert.encode().unwrap();
|
||||
pretty_assertions::assert_eq!(encoded, WINCERT_WITH_X509_CERTIFICATE.to_vec());
|
||||
}
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
# File auto-generated and managed by Devops
|
||||
/.github/ @devolutions/devops @devolutions/architecture-maintainers
|
||||
/.github/dependabot.yml @devolutions/security-managers
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directories:
|
||||
- "/"
|
||||
- "/fuzz/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
assignees:
|
||||
- "CBenoit"
|
||||
open-pull-requests-limit: 3
|
||||
groups:
|
||||
crypto:
|
||||
patterns:
|
||||
- "md-5"
|
||||
- "md5"
|
||||
- "md4"
|
||||
- "sha1"
|
||||
- "sha2"
|
||||
- "sha3"
|
||||
- "pkcs*"
|
||||
- "x509-cert"
|
||||
- "hmac"
|
||||
- "der"
|
||||
- "crypto-mac"
|
||||
- "*tls*"
|
||||
- "*rand*"
|
||||
- "picky*"
|
||||
windows:
|
||||
patterns:
|
||||
- "windows*"
|
||||
- "winapi"
|
||||
patch:
|
||||
dependency-type: "production"
|
||||
update-types:
|
||||
- "patch"
|
||||
dev:
|
||||
dependency-type: "development"
|
||||
+503
@@ -0,0 +1,503 @@
|
||||
name: Build Native
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
preflight:
|
||||
name: Preflight
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
project-version: ${{ steps.get-version.outputs.project-version }}
|
||||
package-version: ${{ steps.get-version.outputs.package-version }}
|
||||
|
||||
steps:
|
||||
- name: Checkout ${{ github.repository }}
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get version
|
||||
id: get-version
|
||||
shell: pwsh
|
||||
run: |
|
||||
$CsprojXml = [Xml] (Get-Content .\ffi\dotnet\Devolutions.Sspi\Devolutions.Sspi.csproj)
|
||||
$ProjectVersion = $CsprojXml.Project.PropertyGroup.Version | Select-Object -First 1
|
||||
$PackageVersion = $ProjectVersion -Replace "^(\d+)\.(\d+)\.(\d+).(\d+)$", "`$1.`$2.`$3"
|
||||
echo "project-version=$ProjectVersion" >> $Env:GITHUB_OUTPUT
|
||||
echo "package-version=$PackageVersion" >> $Env:GITHUB_OUTPUT
|
||||
|
||||
build-native:
|
||||
name: Native build
|
||||
runs-on: ${{ matrix.runner }}
|
||||
needs: preflight
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ win, osx, linux, ios, iossimulator, android ]
|
||||
arch: [ x86, x64, arm, arm64 ]
|
||||
build: [ debug, release ]
|
||||
include:
|
||||
- os: win
|
||||
runner: windows-2022
|
||||
- os: osx
|
||||
runner: macos-14
|
||||
- os: linux
|
||||
runner: ubuntu-22.04
|
||||
- os: ios
|
||||
runner: macos-14
|
||||
- os: iossimulator
|
||||
runner: macos-14
|
||||
- os: android
|
||||
runner: ubuntu-22.04
|
||||
exclude:
|
||||
- arch: arm
|
||||
os: win
|
||||
- arch: arm
|
||||
os: osx
|
||||
- arch: arm
|
||||
os: linux
|
||||
- arch: arm
|
||||
os: ios
|
||||
- arch: x86
|
||||
os: win
|
||||
- arch: x86
|
||||
os: osx
|
||||
- arch: x86
|
||||
os: linux
|
||||
- arch: x86
|
||||
os: ios
|
||||
- arch: x64
|
||||
os: ios
|
||||
- arch: arm
|
||||
os: iossimulator
|
||||
- arch: x86
|
||||
os: iossimulator
|
||||
- arch: x64
|
||||
os: iossimulator
|
||||
|
||||
steps:
|
||||
- name: Checkout ${{ github.repository }}
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Configure Android NDK
|
||||
uses: Devolutions/actions-public/cargo-android-ndk@v1
|
||||
if: matrix.os == 'android'
|
||||
with:
|
||||
android_api_level: "21"
|
||||
|
||||
- name: Install dependencies for rustls
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
choco install ninja nasm
|
||||
|
||||
# We need to add the NASM binary folder to the PATH manually.
|
||||
# We don’t need to do that for ninja.
|
||||
Write-Output "PATH=$Env:PATH;$Env:ProgramFiles\NASM" >> $Env:GITHUB_ENV
|
||||
|
||||
# libclang / LLVM is a requirement for AWS LC.
|
||||
# https://aws.github.io/aws-lc-rs/requirements/windows.html#libclang--llvm
|
||||
$VSINSTALLDIR = $(vswhere.exe -latest -requires Microsoft.VisualStudio.Component.VC.Llvm.Clang -property installationPath)
|
||||
Write-Output "LIBCLANG_PATH=$VSINSTALLDIR\VC\Tools\Llvm\x64\bin" >> $Env:GITHUB_ENV
|
||||
|
||||
# Install Visual Studio Developer PowerShell Module for cmdlets such as Enter-VsDevShell
|
||||
Install-Module VsDevShell -Force
|
||||
|
||||
- name: Install dependencies
|
||||
if: matrix.os == 'ios' || matrix.os == 'iossimulator'
|
||||
run: cargo install --force --locked bindgen-cli
|
||||
|
||||
- name: Setup build environment
|
||||
shell: pwsh
|
||||
run: |
|
||||
if ('${{ matrix.os }}' -Eq 'osx') {
|
||||
echo "MACOSX_DEPLOYMENT_TARGET=10.10" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
|
||||
} elseif ('${{ matrix.os }}' -Eq 'ios') {
|
||||
echo "IPHONEOS_DEPLOYMENT_TARGET=12.1" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
|
||||
} elseif ('${{ matrix.os }}' -Eq 'iossimulator') {
|
||||
echo "IPHONESIMULATOR_DEPLOYMENT_TARGET=12.1" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
|
||||
$SimulatorSdk = xcrun --sdk iphonesimulator --show-sdk-path
|
||||
echo "SDKROOT=$SimulatorSdk" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
|
||||
}
|
||||
|
||||
- name: Setup LLVM
|
||||
uses: Devolutions/actions-public/setup-llvm@v1
|
||||
if: matrix.os == 'linux'
|
||||
with:
|
||||
version: "18.1.8"
|
||||
|
||||
- name: Setup CBake
|
||||
uses: Devolutions/actions-public/setup-cbake@v1
|
||||
if: matrix.os == 'linux'
|
||||
with:
|
||||
sysroots: |
|
||||
- ubuntu-20.04-amd64
|
||||
- ubuntu-20.04-arm64
|
||||
cargo_env_scripts: true
|
||||
|
||||
- name: Build sspi (${{matrix.os}}-${{matrix.arch}}) (${{matrix.build}})
|
||||
shell: pwsh
|
||||
run: |
|
||||
Set-PSDebug -Trace 1
|
||||
|
||||
$BuildType = '${{matrix.build}}'
|
||||
$DotNetOs = '${{matrix.os}}'
|
||||
$DotNetArch = '${{matrix.arch}}'
|
||||
$DotNetRid = '${{matrix.os}}-${{matrix.arch}}'
|
||||
$RustArch = @{'x64'='x86_64';'arm64'='aarch64';
|
||||
'x86'='i686';'arm'='armv7'}[$DotNetArch]
|
||||
$AppleArch = @{'x64'='x86_64';'arm64'='arm64';
|
||||
'x86'='i386';'arm'='arm'}[$DotNetArch]
|
||||
$MsvcArch = @{'x64'='x64';'arm64'='arm64'}[$DotNetArch]
|
||||
$RustPlatform = @{'win'='pc-windows-msvc';
|
||||
'osx'='apple-darwin';'ios'='apple-ios';'iossimulator'='apple-ios-sim';
|
||||
'linux'='unknown-linux-gnu';'android'='linux-android'}[$DotNetOs]
|
||||
$LibPrefix = @{'win'='';'osx'='lib';'ios'='lib';'iossimulator'='lib';
|
||||
'linux'='lib';'android'='lib'}[$DotNetOs]
|
||||
$LibSuffix = @{'win'='.dll';'osx'='.dylib';'ios'='.dylib';'iossimulator'='.dylib';
|
||||
'linux'='.so';'android'='.so'}[$DotNetOs]
|
||||
$RustTarget = "$RustArch-$RustPlatform"
|
||||
$TargetFolder = @{'debug'='debug';'release'='ffi-production'}[$BuildType]
|
||||
|
||||
if (($DotNetOs -eq 'android') -and ($DotNetArch -eq 'arm')) {
|
||||
$RustTarget = "armv7-linux-androideabi"
|
||||
}
|
||||
|
||||
if ($DotNetOs -eq 'osx') {
|
||||
Set-Item "Env:CFLAGS_${RustArch}-apple-darwin" "-arch $AppleArch"
|
||||
}
|
||||
|
||||
rustup target add $RustTarget
|
||||
|
||||
if ($DotNetOs -eq 'win') {
|
||||
Enter-VsDevShell $MsvcArch
|
||||
$Env:RUSTFLAGS="-C target-feature=+crt-static"
|
||||
}
|
||||
|
||||
$ProjectVersion = '${{ needs.preflight.outputs.project-version }}'
|
||||
$PackageVersion = '${{ needs.preflight.outputs.package-version }}'
|
||||
|
||||
$CargoToml = Get-Content .\ffi\Cargo.toml
|
||||
$CargoToml = $CargoToml | ForEach-Object {
|
||||
if ($_.StartsWith("version =")) { "version = `"$PackageVersion`"" } else { $_ }
|
||||
}
|
||||
Set-Content -Path .\ffi\Cargo.toml -Value $CargoToml
|
||||
|
||||
$CargoArgs = @('build', '-vvv', '-p', 'sspi-ffi', '--target', $RustTarget)
|
||||
|
||||
if (-Not ($BuildType -Eq 'debug')) {
|
||||
$CargoArgs += @('--profile', 'ffi-production')
|
||||
}
|
||||
|
||||
if ($DotNetOs -Eq 'win') {
|
||||
$CargoArgs += @('--features', 'tsssp')
|
||||
}
|
||||
|
||||
# No pregenerated Android bindings are provided for aws-lc-sys at this time.
|
||||
# See: https://github.com/aws/aws-lc-rs/tree/main/aws-lc-sys#pregenerated-bindings-availability
|
||||
# For simplicity, we’re using the ring crypto backend.
|
||||
if ($DotNetOs -Eq 'android') {
|
||||
$CargoArgs += @('--no-default-features', '--features', 'scard,ring')
|
||||
}
|
||||
|
||||
if ($DotNetOs -eq 'linux') {
|
||||
$LinuxArch = @{'x64'='amd64';'arm64'='arm64'}[$DotNetArch]
|
||||
$Env:SYSROOT_NAME = "ubuntu-20.04-$LinuxArch"
|
||||
. "$HOME/.cargo/cbake/${RustTarget}-enter.ps1"
|
||||
$Env:AWS_LC_SYS_CMAKE_BUILDER="true"
|
||||
}
|
||||
|
||||
$CargoCmd = $(@('cargo') + $CargoArgs) -Join ' '
|
||||
Write-Host $CargoCmd
|
||||
& cargo $CargoArgs | Out-Host
|
||||
|
||||
$OutputLibraryName = "${LibPrefix}sspi$LibSuffix"
|
||||
$RenamedLibraryName = "${LibPrefix}DevolutionsSspi$LibSuffix"
|
||||
|
||||
$OutputLibrary = Join-Path "target" $RustTarget $TargetFolder $OutputLibraryName
|
||||
$OutputPath = Join-Path "dependencies" "runtimes" $DotNetRid "native"
|
||||
|
||||
New-Item -ItemType Directory -Path $OutputPath | Out-Null
|
||||
Copy-Item $OutputLibrary $(Join-Path $OutputPath $RenamedLibraryName)
|
||||
|
||||
# Export variables for symbol collection step
|
||||
echo "RUST_TARGET=$RustTarget" >> $Env:GITHUB_ENV
|
||||
echo "TARGET_FOLDER=$TargetFolder" >> $Env:GITHUB_ENV
|
||||
echo "LIB_PREFIX=$LibPrefix" >> $Env:GITHUB_ENV
|
||||
echo "DOTNET_OS=$DotNetOs" >> $Env:GITHUB_ENV
|
||||
echo "DOTNET_RID=$DotNetRid" >> $Env:GITHUB_ENV
|
||||
|
||||
- name: Collect debug symbols
|
||||
if: matrix.build == 'release'
|
||||
shell: pwsh
|
||||
run: |
|
||||
Set-PSDebug -Trace 1
|
||||
|
||||
$DotNetOs = $Env:DOTNET_OS
|
||||
$DotNetRid = $Env:DOTNET_RID
|
||||
$RustTarget = $Env:RUST_TARGET
|
||||
$TargetFolder = $Env:TARGET_FOLDER
|
||||
$LibPrefix = $Env:LIB_PREFIX
|
||||
|
||||
$SymbolsPath = Join-Path "symbols" $DotNetRid
|
||||
New-Item -ItemType Directory -Path $SymbolsPath -Force | Out-Null
|
||||
|
||||
$TargetDir = Join-Path "target" $RustTarget $TargetFolder
|
||||
|
||||
if ($DotNetOs -eq 'win') {
|
||||
# Windows: Copy PDB file
|
||||
$PdbFile = Join-Path $TargetDir "sspi.pdb"
|
||||
if (Test-Path $PdbFile) {
|
||||
Copy-Item $PdbFile (Join-Path $SymbolsPath "DevolutionsSspi.pdb")
|
||||
Write-Host "Copied PDB: $PdbFile"
|
||||
} else {
|
||||
Write-Warning "PDB file not found: $PdbFile"
|
||||
Get-ChildItem $TargetDir -Filter "*.pdb" | ForEach-Object { Write-Host "Found PDB: $($_.FullName)" }
|
||||
}
|
||||
} elseif ($DotNetOs -eq 'osx' -or $DotNetOs -eq 'ios' -or $DotNetOs -eq 'iossimulator') {
|
||||
# macOS/iOS: Generate dSYM bundle using dsymutil
|
||||
$DylibFile = Join-Path $TargetDir "${LibPrefix}sspi.dylib"
|
||||
$DsymOutput = Join-Path $SymbolsPath "libDevolutionsSspi.dylib.dSYM"
|
||||
if (Test-Path $DylibFile) {
|
||||
& dsymutil $DylibFile -o $DsymOutput
|
||||
Write-Host "Generated dSYM: $DsymOutput"
|
||||
} else {
|
||||
Write-Warning "Dylib file not found: $DylibFile"
|
||||
}
|
||||
} elseif ($DotNetOs -eq 'linux' -or $DotNetOs -eq 'android') {
|
||||
# Linux/Android: Extract debug info into a separate file
|
||||
$DwpFile = Join-Path $TargetDir "${LibPrefix}sspi.dwp"
|
||||
$SoFile = Join-Path $TargetDir "${LibPrefix}sspi.so"
|
||||
$DebugOutput = Join-Path $SymbolsPath "libDevolutionsSspi.so.debug"
|
||||
if (Test-Path $DwpFile) {
|
||||
# split-debuginfo=packed may produce a .dwp file
|
||||
Copy-Item $DwpFile (Join-Path $SymbolsPath "libDevolutionsSspi.so.dwp")
|
||||
Write-Host "Copied DWP: $DwpFile"
|
||||
} elseif (Test-Path $SoFile) {
|
||||
# Extract debug sections from the binary using objcopy
|
||||
if ($DotNetOs -eq 'android') {
|
||||
$ObjCopy = Join-Path $Env:ANDROID_NDK_HOME "toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-objcopy"
|
||||
if (-not (Test-Path $ObjCopy)) {
|
||||
throw "llvm-objcopy not found at $ObjCopy"
|
||||
}
|
||||
} else {
|
||||
$ObjCopy = 'llvm-objcopy'
|
||||
$ObjCopyCmd = Get-Command $ObjCopy -ErrorAction SilentlyContinue
|
||||
if (-not $ObjCopyCmd) {
|
||||
throw "llvm-objcopy not found on PATH"
|
||||
}
|
||||
}
|
||||
Write-Host "Extracting debug info with: $ObjCopy --only-keep-debug"
|
||||
& $ObjCopy --only-keep-debug $SoFile $DebugOutput 2>&1 | Write-Host
|
||||
Write-Host "Extracted debug info: $DebugOutput"
|
||||
$SizeMB = [math]::Round((Get-Item $DebugOutput).Length / 1MB, 2)
|
||||
Write-Host "Debug file size: $SizeMB MB"
|
||||
} else {
|
||||
Write-Warning "No debug symbols found for Linux/Android"
|
||||
}
|
||||
}
|
||||
|
||||
# List collected symbols
|
||||
Write-Host "Collected symbols:"
|
||||
Get-ChildItem -Recurse $SymbolsPath | ForEach-Object { Write-Host $_.FullName }
|
||||
|
||||
- name: Strip release binaries
|
||||
if: matrix.build == 'release'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$DotNetOs = $Env:DOTNET_OS
|
||||
$DotNetRid = $Env:DOTNET_RID
|
||||
$LibPrefix = $Env:LIB_PREFIX
|
||||
$RuntimePath = Join-Path "dependencies" "runtimes" "$DotNetRid" "native"
|
||||
|
||||
if ($DotNetOs -eq 'win') {
|
||||
# Windows: DLL is already separate from PDB, no stripping needed
|
||||
Write-Host "Windows binaries are already compact (PDB is separate)"
|
||||
} elseif ($DotNetOs -eq 'osx' -or $DotNetOs -eq 'ios' -or $DotNetOs -eq 'iossimulator') {
|
||||
$DylibFile = Join-Path $RuntimePath "${LibPrefix}DevolutionsSspi.dylib"
|
||||
if (Test-Path $DylibFile) {
|
||||
& strip -Sx $DylibFile
|
||||
Write-Host "Stripped dylib: $DylibFile"
|
||||
$SizeMB = [math]::Round((Get-Item $DylibFile).Length / 1MB, 2)
|
||||
Write-Host "Size after stripping: $SizeMB MB"
|
||||
}
|
||||
} elseif ($DotNetOs -eq 'android' -or $DotNetOs -eq 'linux') {
|
||||
$SoFile = Join-Path $RuntimePath "${LibPrefix}DevolutionsSspi.so"
|
||||
if (Test-Path $SoFile) {
|
||||
if ($DotNetOs -eq 'android') {
|
||||
$StripTool = Join-Path $Env:ANDROID_NDK_HOME "toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip"
|
||||
} else {
|
||||
$StripTool = 'llvm-strip'
|
||||
}
|
||||
Write-Host "Using strip tool: $StripTool"
|
||||
& $StripTool --strip-unneeded $SoFile 2>&1 | Write-Host
|
||||
Write-Host "Stripped .so ($DotNetOs): $SoFile"
|
||||
$SizeMB = [math]::Round((Get-Item $SoFile).Length / 1MB, 2)
|
||||
Write-Host "Size after stripping: $SizeMB MB"
|
||||
}
|
||||
}
|
||||
|
||||
- name: Framework (${{matrix.os}}-${{matrix.arch}}) (${{matrix.build}})
|
||||
if: matrix.os == 'ios' || matrix.os == 'iossimulator'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$Version = '${{ needs.preflight.outputs.project-version }}'
|
||||
$ShortVersion = '${{ needs.preflight.outputs.package-version }}'
|
||||
$BundleName = "libDevolutionsSspi"
|
||||
$RuntimesDir = Join-Path "dependencies" "runtimes" "${{ matrix.os }}-${{ matrix.arch }}" "native"
|
||||
$FrameworkDir = Join-Path "$RuntimesDir" "$BundleName.framework"
|
||||
New-Item -Path $FrameworkDir -ItemType "directory" -Force
|
||||
$FrameworkExecutable = Join-Path $FrameworkDir $BundleName
|
||||
Copy-Item -Path (Join-Path "$RuntimesDir" "$BundleName.dylib") -Destination $FrameworkExecutable -Force
|
||||
|
||||
$RPathCmd = $(@('install_name_tool', '-id', "@rpath/$BundleName.framework/$BundleName", "$FrameworkExecutable")) -Join ' '
|
||||
Write-Host $RPathCmd
|
||||
Invoke-Expression $RPathCmd
|
||||
|
||||
[xml] $InfoPlistXml = Get-Content "Info.plist"
|
||||
Select-Xml -xml $InfoPlistXml -XPath "/plist/dict/key[. = 'CFBundleIdentifier']/following-sibling::string[1]" |
|
||||
%{
|
||||
$_.Node.InnerXml = "com.devolutions.sspi"
|
||||
}
|
||||
Select-Xml -xml $InfoPlistXml -XPath "/plist/dict/key[. = 'CFBundleExecutable']/following-sibling::string[1]" |
|
||||
%{
|
||||
$_.Node.InnerXml = $BundleName
|
||||
}
|
||||
Select-Xml -xml $InfoPlistXml -XPath "/plist/dict/key[. = 'CFBundleVersion']/following-sibling::string[1]" |
|
||||
%{
|
||||
$_.Node.InnerXml = $Version
|
||||
}
|
||||
Select-Xml -xml $InfoPlistXml -XPath "/plist/dict/key[. = 'CFBundleShortVersionString']/following-sibling::string[1]" |
|
||||
%{
|
||||
$_.Node.InnerXml = $ShortVersion
|
||||
}
|
||||
|
||||
# Write the plist *without* a BOM
|
||||
$Encoding = New-Object System.Text.UTF8Encoding($false)
|
||||
$Writer = New-Object System.IO.StreamWriter((Join-Path $FrameworkDir "Info.plist"), $false, $Encoding)
|
||||
$InfoPlistXml.Save($Writer)
|
||||
$Writer.Close()
|
||||
|
||||
# .NET XML document inserts two square brackets at the end of the DOCTYPE tag
|
||||
# It's perfectly valid XML, but we're dealing with plists here and dyld will not be able to read the file
|
||||
((Get-Content -Path (Join-Path $FrameworkDir "Info.plist") -Raw) -Replace 'PropertyList-1.0.dtd"\[\]', 'PropertyList-1.0.dtd"') | Set-Content -Path (Join-Path $FrameworkDir "Info.plist")
|
||||
|
||||
- name: Upload native components
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sspi-${{matrix.os}}-${{matrix.arch}}-${{matrix.build}}
|
||||
path: dependencies/runtimes/${{matrix.os}}-${{matrix.arch}}
|
||||
|
||||
- name: Upload debug symbols
|
||||
if: matrix.build == 'release'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sspi-${{matrix.os}}-${{matrix.arch}}-${{matrix.build}}-symbols
|
||||
path: symbols/${{matrix.os}}-${{matrix.arch}}
|
||||
|
||||
build-universal:
|
||||
name: Universal Build
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [preflight, build-native]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ osx ]
|
||||
build: [ debug, release ]
|
||||
|
||||
steps:
|
||||
- name: Checkout ${{ github.repository }}
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup CCTools
|
||||
uses: Devolutions/actions-public/setup-cctools@v1
|
||||
|
||||
- name: Install Apple strip (cctools)
|
||||
if: matrix.build == 'release'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$DownloadVersion = "v2025.2.0"
|
||||
$HostPlatform = "ubuntu-$(lsb_release -rs)"
|
||||
$HostArch = @{'X64'='x86_64';'ARM64'='aarch64'}['${{ runner.arch }}']
|
||||
$PackageBaseName = "cctools-$HostArch-$HostPlatform"
|
||||
$DownloadFile = "${PackageBaseName}.tar.xz"
|
||||
$BaseDownloadUrl = "https://github.com/awakecoding/llvm-prebuilt/releases/download/$DownloadVersion"
|
||||
|
||||
curl -L -o $DownloadFile "${BaseDownloadUrl}/${DownloadFile}"
|
||||
tar -xf $DownloadFile -C /tmp
|
||||
sudo mv "/tmp/${PackageBaseName}/bin/strip" /usr/bin/apple-strip
|
||||
sudo chmod +x /usr/bin/apple-strip
|
||||
Remove-Item $DownloadFile -Force | Out-Null
|
||||
|
||||
- name: Setup LLVM
|
||||
uses: Devolutions/actions-public/setup-llvm@v1
|
||||
if: matrix.build == 'release'
|
||||
with:
|
||||
version: "18.1.8"
|
||||
|
||||
- name: Download native components
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: dependencies/runtimes
|
||||
|
||||
- name: Lipo (${{matrix.build}})
|
||||
shell: pwsh
|
||||
run: |
|
||||
Set-Location "dependencies/runtimes"
|
||||
# No RID for universal binaries, see: https://github.com/dotnet/runtime/issues/53156
|
||||
$OutputPath = Join-Path "${{ matrix.os }}-universal" "native"
|
||||
New-Item -ItemType Directory -Path $OutputPath | Out-Null
|
||||
$Libraries = Get-ChildItem -Recurse -Path "sspi-${{ matrix.os }}-*-${{ matrix.build }}" -Filter "*.dylib" | Foreach-Object { $_.FullName } | Select -Unique
|
||||
$LipoCmd = $(@('lipo', '-create', '-output', (Join-Path -Path $OutputPath -ChildPath "libDevolutionsSspi.dylib")) + $Libraries) -Join ' '
|
||||
Write-Host $LipoCmd
|
||||
Invoke-Expression $LipoCmd
|
||||
|
||||
- name: Generate universal dSYM
|
||||
if: matrix.build == 'release'
|
||||
shell: pwsh
|
||||
run: |
|
||||
Set-PSDebug -Trace 1
|
||||
|
||||
$SymbolsPath = Join-Path "symbols" "${{ matrix.os }}-universal"
|
||||
New-Item -ItemType Directory -Path $SymbolsPath -Force | Out-Null
|
||||
|
||||
$DylibFile = Join-Path "dependencies" "runtimes" "${{ matrix.os }}-universal" "native" "libDevolutionsSspi.dylib"
|
||||
$DsymOutput = Join-Path $SymbolsPath "libDevolutionsSspi.dylib.dSYM"
|
||||
|
||||
if (Test-Path $DylibFile) {
|
||||
& dsymutil $DylibFile -o $DsymOutput
|
||||
Write-Host "Generated universal dSYM: $DsymOutput"
|
||||
} else {
|
||||
Write-Warning "Universal dylib not found: $DylibFile"
|
||||
}
|
||||
|
||||
# List collected symbols
|
||||
Write-Host "Collected symbols:"
|
||||
Get-ChildItem -Recurse $SymbolsPath | ForEach-Object { Write-Host $_.FullName }
|
||||
|
||||
- name: Strip universal binary
|
||||
if: matrix.build == 'release'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$DylibFile = Join-Path "dependencies" "runtimes" "${{ matrix.os }}-universal" "native" "libDevolutionsSspi.dylib"
|
||||
if (Test-Path $DylibFile) {
|
||||
if (-not (Test-Path "/usr/bin/apple-strip")) {
|
||||
throw "apple-strip not found at /usr/bin/apple-strip"
|
||||
}
|
||||
& /usr/bin/apple-strip -Sx $DylibFile
|
||||
Write-Host "Stripped universal dylib with: /usr/bin/apple-strip -Sx"
|
||||
$SizeMB = [math]::Round((Get-Item $DylibFile).Length / 1MB, 2)
|
||||
Write-Host "Size after stripping: $SizeMB MB"
|
||||
}
|
||||
|
||||
- name: Upload native components
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sspi-${{ matrix.os }}-universal-${{ matrix.build }}
|
||||
path: dependencies/runtimes/${{ matrix.os }}-universal
|
||||
|
||||
- name: Upload universal debug symbols
|
||||
if: matrix.build == 'release'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sspi-${{ matrix.os }}-universal-${{ matrix.build }}-symbols
|
||||
path: symbols/${{ matrix.os }}-universal
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [ opened, synchronize, reopened ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
formatting:
|
||||
name: Check formatting
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Check formatting
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Check formatting"
|
||||
cargo fmt --all -- --check
|
||||
if ($LastExitCode -eq 1) {
|
||||
throw "Bad formatting, please run 'cargo +stable fmt --all'"
|
||||
}
|
||||
|
||||
lints:
|
||||
name: Lints [${{ matrix.os }}]
|
||||
runs-on: ${{ matrix.runner }}
|
||||
needs: formatting
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ win, osx, linux ]
|
||||
include:
|
||||
- os: win
|
||||
runner: windows-2022
|
||||
additional-args: --features tsssp
|
||||
- os: osx
|
||||
runner: macos-14
|
||||
- os: linux
|
||||
runner: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
# Compiling the ffi module is enough to lint the whole sspi workspace
|
||||
- name: Check clippy
|
||||
env:
|
||||
AWS_LC_SYS_NO_ASM: true
|
||||
run: cargo clippy --manifest-path ffi/Cargo.toml ${{ matrix.additional-args }} -- -D warnings -D clippy::print_stdout
|
||||
|
||||
tests:
|
||||
name: Tests [${{ matrix.os }}] [${{ matrix.crate-name }}]
|
||||
runs-on: ${{ matrix.runner }}
|
||||
needs: formatting
|
||||
env:
|
||||
SSPI_RS_IS_RUNNING_TESTS: true
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ win, osx, linux ]
|
||||
manifest:
|
||||
- Cargo.toml
|
||||
- ffi/Cargo.toml
|
||||
- crates/dpapi/Cargo.toml
|
||||
- crates/winscard/Cargo.toml
|
||||
- crates/dpapi-pdu/Cargo.toml
|
||||
- crates/ffi-types/Cargo.toml
|
||||
- crates/dpapi-core/Cargo.toml
|
||||
- crates/dpapi-transport/Cargo.toml
|
||||
- crates/dpapi-native-transport/Cargo.toml
|
||||
|
||||
include:
|
||||
# Map runner per OS
|
||||
- os: win
|
||||
runner: windows-2022
|
||||
- os: osx
|
||||
runner: macos-14
|
||||
- os: linux
|
||||
runner: ubuntu-latest
|
||||
|
||||
# Map crate name per manifest
|
||||
- manifest: Cargo.toml
|
||||
crate-name: sspi
|
||||
- manifest: ffi/Cargo.toml
|
||||
crate-name: sspi-ffi
|
||||
- manifest: crates/dpapi/Cargo.toml
|
||||
crate-name: dpapi
|
||||
- manifest: crates/winscard/Cargo.toml
|
||||
crate-name: winscard
|
||||
- manifest: crates/dpapi-pdu/Cargo.toml
|
||||
crate-name: dpapi-pdu
|
||||
- manifest: crates/ffi-types/Cargo.toml
|
||||
crate-name: ffi-types
|
||||
- manifest: crates/dpapi-core/Cargo.toml
|
||||
crate-name: dpapi-core
|
||||
- manifest: crates/dpapi-transport/Cargo.toml
|
||||
crate-name: dpapi-transport
|
||||
- manifest: crates/dpapi-native-transport/Cargo.toml
|
||||
crate-name: dpapi-native-transport
|
||||
|
||||
# Per-OS feature overrides for specific manifests.
|
||||
# `__test-data` enables the `tests/sspi/client_server` suite (gated on
|
||||
# `all(network_client, __test-data)`), which otherwise never runs in CI.
|
||||
- os: win
|
||||
manifest: Cargo.toml
|
||||
additional-args: --features network_client,dns_resolver,scard,tsssp,__test-data
|
||||
- os: osx
|
||||
manifest: Cargo.toml
|
||||
additional-args: --features network_client,scard,__test-data
|
||||
- os: linux
|
||||
manifest: Cargo.toml
|
||||
additional-args: --features network_client,dns_resolver,scard,__test-data
|
||||
|
||||
- os: win
|
||||
manifest: ffi/Cargo.toml
|
||||
additional-args: --features tsssp
|
||||
|
||||
- os: win
|
||||
manifest: crates/dpapi/Cargo.toml
|
||||
additional-args: --features tsssp
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Test
|
||||
env:
|
||||
AWS_LC_SYS_NO_ASM: true
|
||||
run: cargo test --manifest-path ${{ matrix.manifest }} ${{ matrix.additional-args }}
|
||||
|
||||
miri:
|
||||
name: Miri FFI tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: formatting
|
||||
env:
|
||||
SSPI_RS_IS_RUNNING_TESTS: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
# NOTE: Pinned to a specific nightly version for reproducibility.
|
||||
# Update this manually on a regular basis.
|
||||
- name: Install nightly toolchain and Miri
|
||||
run: |
|
||||
rustup toolchain install nightly-2026-02-11 --component miri
|
||||
rustup override set nightly-2026-02-11
|
||||
|
||||
- name: Test
|
||||
run: cargo miri test --manifest-path ffi/Cargo.toml --no-default-features --features ring
|
||||
|
||||
wasm:
|
||||
name: WASM target
|
||||
runs-on: ubuntu-latest
|
||||
needs: formatting
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Prepare runner
|
||||
run: sudo apt-get install wabt
|
||||
|
||||
- name: Check
|
||||
shell: pwsh
|
||||
run: ./tools/wasm-testcompile/check.ps1
|
||||
|
||||
build-native:
|
||||
name: Build native
|
||||
needs: formatting
|
||||
uses: ./.github/workflows/build-native.yml
|
||||
|
||||
success:
|
||||
name: Success
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ always() }}
|
||||
needs:
|
||||
- formatting
|
||||
- lints
|
||||
- tests
|
||||
- miri
|
||||
- wasm
|
||||
- build-native
|
||||
|
||||
steps:
|
||||
- name: CI succeeded
|
||||
id: succeeded
|
||||
if: ${{ !contains(needs.*.result, 'failure') }}
|
||||
run: exit 0
|
||||
|
||||
- name: CI failed
|
||||
if: ${{ steps.succeeded.outcome == 'skipped' }}
|
||||
run: exit 1
|
||||
+322
@@ -0,0 +1,322 @@
|
||||
name: Publish NuGet package
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry-run:
|
||||
description: 'Dry run'
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
version:
|
||||
description: 'Package version'
|
||||
default: "latest"
|
||||
required: true
|
||||
schedule:
|
||||
- cron: '21 3 * * 1' # 3:21 AM UTC every Monday
|
||||
|
||||
jobs:
|
||||
preflight:
|
||||
name: Preflight
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
dry-run: ${{ steps.get-dry-run.outputs.dry-run }}
|
||||
package-version: ${{ steps.info.outputs.package-version }}
|
||||
|
||||
steps:
|
||||
- name: Get dry run
|
||||
id: get-dry-run
|
||||
shell: pwsh
|
||||
run: |
|
||||
Set-PSDebug -Trace 1
|
||||
|
||||
$IsDryRun = '${{ github.event.inputs.dry-run }}' -Eq 'true' -Or '${{ github.event_name }}' -Eq 'schedule'
|
||||
|
||||
if ($IsDryRun) {
|
||||
echo "dry-run=true" >> $Env:GITHUB_OUTPUT
|
||||
} else {
|
||||
echo "dry-run=false" >> $Env:GITHUB_OUTPUT
|
||||
}
|
||||
|
||||
- name: Package information
|
||||
id: info
|
||||
shell: pwsh
|
||||
run: |
|
||||
$PackageVersion = '${{ inputs.version }}'
|
||||
if ([string]::IsNullOrEmpty($PackageVersion) -or $PackageVersion -eq 'latest') {
|
||||
$PackageVersion = (Get-Date -Format "yyyy.MM.dd") + ".0"
|
||||
}
|
||||
|
||||
if ($PackageVersion -NotMatch '^\d+\.\d+\.\d+\.\d+$') {
|
||||
throw "invalid version format: $PackageVersion, expected: 1.2.3.4"
|
||||
}
|
||||
|
||||
echo "package-version=$PackageVersion" >> $Env:GITHUB_OUTPUT
|
||||
echo "::notice::Version: $PackageVersion"
|
||||
|
||||
build-native:
|
||||
uses: ./.github/workflows/build-native.yml
|
||||
|
||||
build-managed:
|
||||
name: Managed build
|
||||
runs-on: windows-2022
|
||||
needs: [ preflight, build-native ]
|
||||
|
||||
steps:
|
||||
- name: Check out ${{ github.repository }}
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Prepare dependencies
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Path "dependencies/runtimes" | Out-Null
|
||||
|
||||
- name: Download native components
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: dependencies/runtimes
|
||||
|
||||
- name: Rename dependencies
|
||||
shell: pwsh
|
||||
run: |
|
||||
Set-PSDebug -Trace 1
|
||||
|
||||
Set-Location "dependencies/runtimes"
|
||||
|
||||
$(Get-Item ".\sspi-*-release") | ForEach-Object { Rename-Item $_ $_.Name.Replace("-release", "") }
|
||||
$(Get-Item ".\sspi-*") | ForEach-Object { Rename-Item $_ $_.Name.Replace("sspi-", "") }
|
||||
Get-ChildItem * -Recurse
|
||||
|
||||
- name: Set package version
|
||||
shell: pwsh
|
||||
run: |
|
||||
$PackageVersion = '${{ needs.preflight.outputs.package-version }}'
|
||||
$csprojPath = "ffi\dotnet\Devolutions.Sspi\Devolutions.Sspi.csproj"
|
||||
$csprojContent = Get-Content $csprojPath -Raw
|
||||
$csprojContent = $csprojContent -Replace '(<Version>).*?(</Version>)', "<Version>$PackageVersion</Version>"
|
||||
Set-Content -Path $csprojPath -Value $csprojContent -Encoding UTF8
|
||||
|
||||
- name: Build sspi (managed)
|
||||
shell: pwsh
|
||||
run: |
|
||||
dotnet build .\ffi\dotnet\Devolutions.Sspi\Devolutions.Sspi.csproj -o package
|
||||
|
||||
- name: Upload managed components
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sspi-nupkg
|
||||
path: package/*.nupkg
|
||||
|
||||
publish:
|
||||
name: Publish NuGet package
|
||||
runs-on: ubuntu-latest
|
||||
environment: nuget-publish
|
||||
if: needs.preflight.outputs.dry-run == 'false'
|
||||
needs:
|
||||
- preflight
|
||||
- build-managed
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Download NuGet package artifact
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sspi-nupkg
|
||||
path: package
|
||||
|
||||
- name: NuGet login (OIDC)
|
||||
uses: NuGet/login@v1
|
||||
id: nuget-login
|
||||
with:
|
||||
user: ${{ secrets.NUGET_BOT_USERNAME }}
|
||||
|
||||
- name: Publish to nuget.org
|
||||
shell: pwsh
|
||||
run: |
|
||||
Set-PSDebug -Trace 1
|
||||
|
||||
$Files = Get-ChildItem -Recurse package/*.nupkg
|
||||
|
||||
foreach ($File in $Files) {
|
||||
$PushCmd = @(
|
||||
'dotnet',
|
||||
'nuget',
|
||||
'push',
|
||||
"$File",
|
||||
'--api-key',
|
||||
'${{ steps.nuget-login.outputs.NUGET_API_KEY }}',
|
||||
'--source',
|
||||
'https://api.nuget.org/v3/index.json',
|
||||
'--skip-duplicate'
|
||||
)
|
||||
|
||||
Write-Host "Publishing $($File.Name)..."
|
||||
$PushCmd = $PushCmd -Join ' '
|
||||
Invoke-Expression $PushCmd
|
||||
}
|
||||
|
||||
package-symbols:
|
||||
name: Package debug symbols
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ preflight, build-native ]
|
||||
|
||||
steps:
|
||||
- name: Download all symbol artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: artifacts
|
||||
pattern: sspi-*-release-symbols
|
||||
|
||||
- name: Package symbols into zip
|
||||
shell: pwsh
|
||||
run: |
|
||||
Set-PSDebug -Trace 1
|
||||
|
||||
$PackageVersion = '${{ needs.preflight.outputs.package-version }}'
|
||||
$SymbolsDir = "symbols"
|
||||
New-Item -ItemType Directory -Path $SymbolsDir -Force | Out-Null
|
||||
|
||||
# Organize symbols by RID
|
||||
$SymbolArtifacts = Get-ChildItem -Directory "artifacts" -Filter "sspi-*-release-symbols"
|
||||
foreach ($Artifact in $SymbolArtifacts) {
|
||||
# Extract RID from artifact name (e.g., sspi-win-x64-release-symbols -> win-x64)
|
||||
$Rid = $Artifact.Name -Replace '^sspi-(.+)-release-symbols$', '$1'
|
||||
$DestDir = Join-Path $SymbolsDir $Rid
|
||||
New-Item -ItemType Directory -Path $DestDir -Force | Out-Null
|
||||
|
||||
# Copy all contents from the artifact
|
||||
Get-ChildItem -Path $Artifact.FullName -Recurse | ForEach-Object {
|
||||
if (-not $_.PSIsContainer) {
|
||||
$RelativePath = $_.FullName.Substring($Artifact.FullName.Length + 1)
|
||||
$DestPath = Join-Path $DestDir $RelativePath
|
||||
$DestParent = Split-Path $DestPath -Parent
|
||||
if (-not (Test-Path $DestParent)) {
|
||||
New-Item -ItemType Directory -Path $DestParent -Force | Out-Null
|
||||
}
|
||||
Copy-Item $_.FullName $DestPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Symbol directory structure:"
|
||||
Get-ChildItem -Recurse $SymbolsDir | ForEach-Object { Write-Host $_.FullName }
|
||||
|
||||
# Create the zip file
|
||||
$ZipName = "Devolutions.Sspi.$PackageVersion.symbols.zip"
|
||||
Compress-Archive -Path "$SymbolsDir/*" -DestinationPath $ZipName -Force
|
||||
Write-Host "Created symbols archive: $ZipName"
|
||||
|
||||
- name: Upload symbols zip
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sspi-symbols-zip
|
||||
path: Devolutions.Sspi.*.symbols.zip
|
||||
|
||||
create-release:
|
||||
name: Create GitHub Release
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.preflight.outputs.dry-run == 'false'
|
||||
needs:
|
||||
- preflight
|
||||
- build-managed
|
||||
- package-symbols
|
||||
- publish
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download NuGet package
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sspi-nupkg
|
||||
path: release-assets
|
||||
|
||||
- name: Download symbols zip
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sspi-symbols-zip
|
||||
path: release-assets
|
||||
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
Set-PSDebug -Trace 1
|
||||
|
||||
$PackageVersion = '${{ needs.preflight.outputs.package-version }}'
|
||||
$TagName = "v$PackageVersion"
|
||||
$ReleaseName = "Devolutions.Sspi $PackageVersion"
|
||||
|
||||
# Get the list of assets to upload
|
||||
$Assets = Get-ChildItem -Path "release-assets" -File
|
||||
Write-Host "Release assets:"
|
||||
$Assets | ForEach-Object { Write-Host " - $($_.Name)" }
|
||||
|
||||
# Build the release notes
|
||||
$ReleaseNotes = @"
|
||||
## Devolutions.Sspi $PackageVersion
|
||||
|
||||
### NuGet Package
|
||||
- [Devolutions.Sspi on NuGet.org](https://www.nuget.org/packages/Devolutions.Sspi/$PackageVersion)
|
||||
|
||||
### Included Platforms
|
||||
- Windows (x64, arm64)
|
||||
- macOS (x64, arm64, universal)
|
||||
- Linux (x64, arm64)
|
||||
- iOS device (arm64)
|
||||
- iOS Simulator (arm64)
|
||||
- Android (x86, x64, arm, arm64)
|
||||
|
||||
### Debug Symbols
|
||||
The `Devolutions.Sspi.$PackageVersion.symbols.zip` archive contains debug symbols for all platforms:
|
||||
- Windows: `.pdb` files
|
||||
- macOS/iOS: `.dSYM` bundles
|
||||
- Linux/Android: `.debug` or `.dwp` files
|
||||
"@
|
||||
|
||||
# Create the release with assets
|
||||
$AssetArgs = @()
|
||||
foreach ($Asset in $Assets) {
|
||||
$AssetArgs += $Asset.FullName
|
||||
}
|
||||
|
||||
gh release create $TagName `
|
||||
--title $ReleaseName `
|
||||
--notes $ReleaseNotes `
|
||||
--target ${{ github.sha }} `
|
||||
@AssetArgs
|
||||
|
||||
Write-Host "Created release: $ReleaseName with tag $TagName"
|
||||
|
||||
notify:
|
||||
name: Notify failure
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ always() && contains(needs.*.result, 'failure') && github.event_name == 'schedule' }}
|
||||
needs:
|
||||
- preflight
|
||||
- build-native
|
||||
- build-managed
|
||||
- package-symbols
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_ARCHITECTURE }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
steps:
|
||||
- name: Send slack notification
|
||||
id: slack
|
||||
uses: slackapi/slack-github-action@v1.26.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*${{ github.repository }}* :fire::fire::fire::fire::fire: \n The scheduled build for *${{ github.repository }}* is <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|broken>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
name: Release crates
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
# Create a PR with the new versions and changelog, preparing the next release.
|
||||
open-pr:
|
||||
name: Open release PR
|
||||
environment: cratesio-publish
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: release-plz-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 512
|
||||
|
||||
- name: Run release-plz
|
||||
id: release-plz
|
||||
uses: Devolutions/actions-public/release-plz@v1
|
||||
with:
|
||||
command: release-pr
|
||||
git-name: Devolutions Bot
|
||||
git-email: bot@devolutions.net
|
||||
github-token: ${{ secrets.DEVOLUTIONSBOT_WRITE_TOKEN }}
|
||||
|
||||
# Release unpublished packages.
|
||||
release:
|
||||
name: Release crates
|
||||
environment: cratesio-publish
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write # Required to push the version tag.
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 512
|
||||
|
||||
- name: Authenticate with crates.io
|
||||
id: auth
|
||||
uses: rust-lang/crates-io-auth-action@v1
|
||||
|
||||
- name: Run release-plz
|
||||
uses: Devolutions/actions-public/release-plz@v1
|
||||
with:
|
||||
command: release
|
||||
registry-token: ${{ steps.auth.outputs.token }}
|
||||
@@ -0,0 +1,405 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
|
||||
Properties/
|
||||
|
||||
target
|
||||
**/*.rs.bk
|
||||
.idea/
|
||||
.DS_Store
|
||||
Vendored
+407
@@ -0,0 +1,407 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [[0.21.3](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.21.2...sspi-v0.21.3)] - 2026-07-16
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- Add format-tagged Username::parts view ([#709](https://github.com/Devolutions/sspi-rs/issues/709)) ([80492e4c61](https://github.com/Devolutions/sspi-rs/commit/80492e4c616f7e475391d9b877759b463733fe82))
|
||||
|
||||
Add `UsernameParts`, a format-tagged, borrowed view into a `Username`,
|
||||
and `Username::parts()` returning it. Each variant only exposes the
|
||||
fields that are meaningful for its user name format, so a UPN suffix can
|
||||
no longer be mistaken for a NetBIOS domain: the `UserPrincipalName` arm
|
||||
has a `suffix` (and the full `upn`) but no "domain" field, while
|
||||
`DownLevelLogonName` carries an `Option<netbios_domain>` that models
|
||||
accurately the no-separator case. Matching is exhaustive over the two
|
||||
Microsoft user name formats.
|
||||
|
||||
## [[0.21.2](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.21.1...sspi-v0.21.2)] - 2026-07-15
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Classify UPN keytab principals as NT_ENTERPRISE ([#475](https://github.com/Devolutions/sspi-rs/issues/475)) ([491cf36fea](https://github.com/Devolutions/sspi-rs/commit/491cf36feafb01dac9490842d2472acc3602ffcb))
|
||||
|
||||
The keytab credentials branch passed keytab.principal.account_name() to
|
||||
get_client_principal_name_type, which strips the @suffix of a UPN. Without
|
||||
the @, the principal was misclassified as NT_PRINCIPAL instead of
|
||||
NT_ENTERPRISE (MS-KILE 3.3.5.6.1).
|
||||
|
||||
- Avoid panic on short SRV record RDATA on macOS ([#706](https://github.com/Devolutions/sspi-rs/issues/706)) ([a4ab1bbf0c](https://github.com/Devolutions/sspi-rs/commit/a4ab1bbf0c1c214e921a3189264885c7c278e909))
|
||||
|
||||
## [[0.21.1](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.21.0...sspi-v0.21.1)] - 2026-06-26
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- Add support for SECPKG_ATTR_NAMES ([#676](https://github.com/Devolutions/sspi-rs/issues/676)) ([be6315d475](https://github.com/Devolutions/sspi-rs/commit/be6315d475d7c4ce4c571220e2cfd7cb0881b7c7))
|
||||
|
||||
- Add keytab client credentials and standards-compliant SASL/GSSAPI acceptor support ([#681](https://github.com/Devolutions/sspi-rs/issues/681)) ([d95f249622](https://github.com/Devolutions/sspi-rs/commit/d95f2496226d45fd4d02437c0c84afe870d9f510))
|
||||
|
||||
Extends the `sspi` Kerberos implementation to interoperate with standards-compliant SASL/GSSAPI (RFC 4752 / RFC 4121) peers by adding keytab-backed client credentials, supporting integrity-only (unsealed) `GSS_Wrap` tokens, and allowing acceptors to validate tickets for multiple configured service principals.
|
||||
|
||||
- Support for Kerberos cross realm referral ([#694](https://github.com/Devolutions/sspi-rs/issues/694)) ([797365417f](https://github.com/Devolutions/sspi-rs/commit/797365417f6f75ee175eb0f8eb01d0c0b907d01f))
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Ntlm: NTLM_SSP_NEGOTIATE_KEY_EXCH flag usage ([#670](https://github.com/Devolutions/sspi-rs/issues/670)) ([a26a3692b0](https://github.com/Devolutions/sspi-rs/commit/a26a3692b029c5617f75a6dc618e5c7f78d3bc1b))
|
||||
|
||||
- Package list handling ([#677](https://github.com/Devolutions/sspi-rs/issues/677)) ([ee75161bcd](https://github.com/Devolutions/sspi-rs/commit/ee75161bcdce0998b9173701129c0c7d607e1f17))
|
||||
|
||||
- Disable `mechListMIC` for NTLM guest logon in negotiate module ([#680](https://github.com/Devolutions/sspi-rs/issues/680)) ([80bdedf387](https://github.com/Devolutions/sspi-rs/commit/80bdedf387da7341b95ff947d3bf68f48a0bc404))
|
||||
|
||||
- Read channel bindings on the client authenticate step ([#695](https://github.com/Devolutions/sspi-rs/issues/695)) ([82741f6630](https://github.com/Devolutions/sspi-rs/commit/82741f6630c9b6038064caf8f508ac50e9f59738))
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Advance RustCrypto pins to the current rc.18/rc.33/rc.10 cluster ([#692](https://github.com/Devolutions/sspi-rs/issues/692)) ([095e3f341c](https://github.com/Devolutions/sspi-rs/commit/095e3f341c74d57daafb015bfb0226188e9e8e18))
|
||||
|
||||
- Update RC and crypto dependencies to latest versions ([#699](https://github.com/Devolutions/sspi-rs/issues/699)) ([c5922db0f4](https://github.com/Devolutions/sspi-rs/commit/c5922db0f4921323856a041c5e2987e569e62056))
|
||||
|
||||
## [[0.21.0](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.20.1...sspi-v0.21.0)] - 2026-05-06
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- [**breaking**] Remove redundant PKCS#1 padding when signing hashes ([#660](https://github.com/Devolutions/sspi-rs/issues/660)) ([9e8a108f02](https://github.com/Devolutions/sspi-rs/commit/9e8a108f02704fdbcab23d0fadb2c0d3f5514c0e))
|
||||
|
||||
|
||||
|
||||
## [[0.20.1](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.20.0...sspi-v0.20.1)] - 2026-04-29
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Upgrade picky and RustCrypto dependencies ([#655](https://github.com/Devolutions/sspi-rs/issues/655)) ([26765bbe1f](https://github.com/Devolutions/sspi-rs/commit/26765bbe1f57f490ba1c37703eda8267c3b2dfb6))
|
||||
|
||||
|
||||
|
||||
## [[0.20.0](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.19.2...sspi-v0.20.0)] - 2026-04-27
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- [**breaking**] Remove legacy UTF-16 helpers ([#648](https://github.com/Devolutions/sspi-rs/issues/648)) ([b8f21e1bf3](https://github.com/Devolutions/sspi-rs/commit/b8f21e1bf33eea2f66a1818c3c190bca5be6e568))
|
||||
|
||||
- Add KDC connect timeout to prevent 75s hang on unreachable KDC ([#647](https://github.com/Devolutions/sspi-rs/issues/647)) ([abb9ffe16d](https://github.com/Devolutions/sspi-rs/commit/abb9ffe16d86941290466c7a53b56d1f0b738f18))
|
||||
|
||||
|
||||
## [[0.19.2](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.19.1...sspi-v0.19.2)] - 2026-03-26
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Fall back to NTLM when the target SPN is an IP address ([#637](https://github.com/Devolutions/sspi-rs/issues/637)) ([67dbac66cc](https://github.com/Devolutions/sspi-rs/commit/67dbac66cc98cb4fcec9864f345290e52745122e))
|
||||
|
||||
## [[0.19.1](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.19.0...sspi-v0.19.1)] - 2026-03-23
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Improve NTLM fallback ([#627](https://github.com/Devolutions/sspi-rs/issues/627)) ([88bb31796c](https://github.com/Devolutions/sspi-rs/commit/88bb31796c9bfb0d248b6e32eedbd166d8603679))
|
||||
|
||||
Improved SPNEGO/Negotiate authentication resilience by introducing
|
||||
an explicit Kerberos-first attempt with a controlled fallback to NTLM for
|
||||
a defined set of Kerberos failure conditions (per the referenced FreeRDP
|
||||
behavior), and updates the Kerberos/SPNEGO token flow to support
|
||||
that behavior reliably.
|
||||
|
||||
- Handle ISC_REQ_USE_DCE_STYLE and SECBUFFER_READONLY in EncryptMessage ([#629](https://github.com/Devolutions/sspi-rs/issues/629)) ([93e962be78](https://github.com/Devolutions/sspi-rs/commit/93e962be78d6b43bf41e9e30d4935903786602c7))
|
||||
|
||||
Adjusts the NTLM SSPI implementation to better interoperate with RPC/FreeRDP over RD Gateway by (a) treating `ISC_REQ_USE_DCE_STYLE` as implying sealing/signing, and (b) allowing `EncryptMessage` to operate in sign-only mode when all `SECBUFFER_DATA` buffers are marked `SECBUFFER_READONLY`.
|
||||
|
||||
## [[0.19.0](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.18.9...sspi-v0.19.0)] - 2026-03-13
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- [**breaking**] Introduce UTF-16 strings helpers and refactor auth identity types ([#606](https://github.com/Devolutions/sspi-rs/issues/606)) ([1bfee5e545](https://github.com/Devolutions/sspi-rs/commit/1bfee5e545c4a97ae631fe734b3bcf45b4b486f9))
|
||||
|
||||
## [[0.18.9](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.18.8...sspi-v0.18.9)] - 2026-03-06
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- Support multiple credential candidates ([#611](https://github.com/Devolutions/sspi-rs/issues/611)) ([2ded76b767](https://github.com/Devolutions/sspi-rs/commit/2ded76b76734110d45de2270d3c782c561fe54d1))
|
||||
|
||||
Enable the CredSSP server-side NTLM flow to authenticate against a bounded set of
|
||||
concurrently-valid credential candidates for a single user, selecting the one
|
||||
the client proves knowledge of.
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Include full error source chain in reqwest client build error ([#621](https://github.com/Devolutions/sspi-rs/issues/621)) ([3ba4886776](https://github.com/Devolutions/sspi-rs/commit/3ba4886776600f06b1a338b06ed4f828bd001588))
|
||||
|
||||
- Remove NTLM password length heuristic ([#619](https://github.com/Devolutions/sspi-rs/issues/619)) ([c607a229da](https://github.com/Devolutions/sspi-rs/commit/c607a229da8f5fd394f4be4c26fe7e5b90805df7))
|
||||
|
||||
`compute_ntlm_v2_hash` treats passwords longer than 512 bytes as
|
||||
pre-computed NT hashes and tries to decode them with
|
||||
`convert_password_hash`. This silently breaks authentication for any
|
||||
plaintext password that exceeds that threshold (e.g. long JWT tokens).
|
||||
|
||||
## [[0.18.8](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.18.7...sspi-v0.18.8)] - 2026-02-26
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- Improve domain->realm mapping in kerberos backend ([#604](https://github.com/Devolutions/sspi-rs/issues/604)) ([50cff70f0e](https://github.com/Devolutions/sspi-rs/commit/50cff70f0e64df4630320883f3846872a68d7e27))
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Correct Negotiate (SPNEGO) implementation ([#600](https://github.com/Devolutions/sspi-rs/issues/600)) ([c016e18863](https://github.com/Devolutions/sspi-rs/commit/c016e188633b202c1de7fd84ae1f4f7253fe5dd2))
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Update dependencies
|
||||
|
||||
## [[0.18.7](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.18.6...sspi-v0.18.7)] - 2026-01-16
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- NT Hash Authentication ([#585](https://github.com/Devolutions/sspi-rs/issues/585)) ([21b45e16dc](https://github.com/Devolutions/sspi-rs/commit/21b45e16dcee7171cd9ae70df37cee8cdf41df0e))
|
||||
|
||||
Adds support for NT hash (pass-the-hash) authentication,
|
||||
allowing authentication using pre-computed NT hashes instead of
|
||||
plaintext passwords.
|
||||
|
||||
## [[0.18.6](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.18.5...sspi-v0.18.6)] - 2026-01-05
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Accept variable-length MsvAvSingleHost AvPair ([#580](https://github.com/Devolutions/sspi-rs/issues/580)) ([28f8d74b8c](https://github.com/Devolutions/sspi-rs/commit/28f8d74b8c4f982e047f0bc7738498ac217f6553))
|
||||
|
||||
Windows 11 Build 26200+ sends an 80-byte MsvAvSingleHost structure
|
||||
instead of the traditional 48 bytes. Per MS-NLMP specification, fields
|
||||
after MachineID MUST be ignored on receipt.
|
||||
|
||||
This change:
|
||||
- Changes SingleHost from fixed [u8; 48] to Vec<u8>
|
||||
- Relaxes validation from == 48 to >= 48 bytes
|
||||
- Preserves full data for round-trip serialization
|
||||
|
||||
Fixes RDP credential injection failures when clients use Windows 11
|
||||
Build 26200 or later.
|
||||
|
||||
## [[0.18.5](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.18.4...sspi-v0.18.5)] - 2025-12-11
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Don't use eprintln on unknown packages ([#558](https://github.com/Devolutions/sspi-rs/issues/558)) ([df6181291c](https://github.com/Devolutions/sspi-rs/commit/df6181291ccf6f9fb2a0a59151b43676663b2d74))
|
||||
|
||||
- Use `String::from_utf16_lossy` over `String::from_utf16` to avoid changing the meaning of some buffers in case of invalid UTF-16 input ([#568](https://github.com/Devolutions/sspi-rs/issues/568)) ([a4889f5b1e](https://github.com/Devolutions/sspi-rs/commit/a4889f5b1e802395b09954f5846d1a6433546281))
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Update dependencies
|
||||
|
||||
## [[0.18.4](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.18.3...sspi-v0.18.4)] - 2025-11-18
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Exclude cryptoki for WASM ([#545](https://github.com/Devolutions/sspi-rs/issues/545)) ([9e4a84a9ee](https://github.com/Devolutions/sspi-rs/commit/9e4a84a9ee07c81da07e799e3e209094c9045c83))
|
||||
|
||||
This fixes builds for the wasm32 target.
|
||||
|
||||
## [[0.18.3](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.18.2...sspi-v0.18.3)] - 2025-11-07
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- Add `NT_ENTERPRISE` support in server-side Kerberos ([#535](https://github.com/Devolutions/sspi-rs/issues/535)) ([40785e3123](https://github.com/Devolutions/sspi-rs/commit/40785e3123e381180ac9a75e99248a0287fc5d71))
|
||||
|
||||
This is needed when want to connect using FQDN instead of down-level logon name.
|
||||
|
||||
### <!-- 3 -->Revert
|
||||
|
||||
- Implement `Default` for `SmartCardType` ([#534](https://github.com/Devolutions/sspi-rs/issues/534)) ([7280f7a67b](https://github.com/Devolutions/sspi-rs/commit/7280f7a67b100719052ade72333d373bf956541f))
|
||||
|
||||
It actually does not make sense to implement Default for SmartCardType.
|
||||
A user-provided PIN must be set.
|
||||
|
||||
Release 0.18.2 was yanked.
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- TLS 1.3 support in TSSSP module ([#536](https://github.com/Devolutions/sspi-rs/issues/536)) ([0605cf01f8](https://github.com/Devolutions/sspi-rs/commit/0605cf01f861aca8191e3134fb885bde5eb50506))
|
||||
|
||||
* Adds `CipherSuite::TLS13_AES_256_GCM_SHA384` support.
|
||||
* Fixes TLS packet header validation: TLS 1.3 uses TLS 1.2 version in the packet header.
|
||||
|
||||
- Pin leftover pre-release crypto crates ([#538](https://github.com/Devolutions/sspi-rs/issues/538)) ([6fc91fa977](https://github.com/Devolutions/sspi-rs/commit/6fc91fa977d217b04cdce23e66d56a8a398988e3))
|
||||
|
||||
The patch version upgrade is not allowed to bring breaking changes, but
|
||||
this rule doesn't work for an `rc` version. So we should pin the `rc`
|
||||
versions, to not allow _cargo_ update to a new `rc` automatically.
|
||||
|
||||
## [[0.18.2](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.18.1...sspi-v0.18.2)] - 2025-11-04 (Yanked)
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- Implement `Default` for `SmartCardType` ([#532](https://github.com/Devolutions/sspi-rs/issues/532)) ([3555b377e8](https://github.com/Devolutions/sspi-rs/commit/3555b377e8aeb5a8c376b2ecec9dcb1a2fd79560))
|
||||
|
||||
## [[0.18.1](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.18.0...sspi-v0.18.1)] - 2025-10-29
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Bump hickory-resolver from 0.24.4 to 0.25.2 ([#426](https://github.com/Devolutions/sspi-rs/issues/426)) ([59857e66c2](https://github.com/Devolutions/sspi-rs/commit/59857e66c29e9130af2d24e3c7d79858ce1b55cd))
|
||||
|
||||
## [[0.18.0](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.17.0...sspi-v0.18.0)] - 2025-10-14
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- DH client default parameters: remove leading zero ([#514](https://github.com/Devolutions/sspi-rs/issues/514)) ([8114b570bc](https://github.com/Devolutions/sspi-rs/commit/8114b570bcfa4320b4972c300705a5e93e762965))
|
||||
|
||||
- [**breaking**] Async network client returns `!Send` future ([#513](https://github.com/Devolutions/sspi-rs/issues/513)) ([218ddf3e79](https://github.com/Devolutions/sspi-rs/commit/218ddf3e792580caf1f094ff197a3790f61248e5))
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Bump picky to the latest version ([#516](https://github.com/Devolutions/sspi-rs/issues/516)) ([972e04b153](https://github.com/Devolutions/sspi-rs/commit/972e04b15393acf2028ae7fde0f55d104e8a9294))
|
||||
|
||||
- Bump the patch group across 1 directory with 2 updates ([#519](https://github.com/Devolutions/sspi-rs/issues/519)) ([aaa6e78617](https://github.com/Devolutions/sspi-rs/commit/aaa6e78617b17ee0870c3044f2ac6922c6831017))
|
||||
|
||||
## [[0.17.0](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.16.1...sspi-v0.17.0)] - 2025-10-06
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- Data sigining using scard (#491) ([6728fb525c](https://github.com/Devolutions/sspi-rs/commit/6728fb525cedc96b395eed5dc4a8ea357b036b36))
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- [**breaking**] Move `cert_utils` from `sspi` crate to `ffi` (#507) ([c9337c8f64](https://github.com/Devolutions/sspi-rs/commit/c9337c8f64a6236396242275847989a35db735ac))
|
||||
|
||||
- [**breaking**] Fix lifetimes in initialize_security_context_impl (#495) ([370951c1b0](https://github.com/Devolutions/sspi-rs/commit/370951c1b017bfef4276185b374345e8b6b1e532))
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Bump the windows crates
|
||||
|
||||
- Bump the crypto dependencies (#489) ([1ecba764ec](https://github.com/Devolutions/sspi-rs/commit/1ecba764ec3b04e147ae76d018414afa8bec5f88))
|
||||
|
||||
## [[0.16.1](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.16.0...sspi-v0.16.1)] - 2025-08-19
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- Add method to set the channel bindings for a session (#479) ([0c0e225fe7](https://github.com/Devolutions/sspi-rs/commit/0c0e225fe7d7ddffc18ad0176deba207edaf3524))
|
||||
|
||||
Setting and sending the CBs is described in Sec. 3.1.5.2.1 of the NTLM spec, admittedly in a slightly confusing way, which may seem to suggest that the bindings are somehow part of the CHALLENGE message, but they are not: knowledge of CBs is strictly local to the client, therefore the client should send them in AUTHENTICATE.
|
||||
|
||||
## [[0.16.0](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.15.14...sspi-v0.16.0)] - 2025-07-07
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- Server-side Kerberos implementation (#440) ([943a297edd](https://github.com/Devolutions/sspi-rs/commit/943a297eddad91bf6dfa02bdb53b422453df0ed9))
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Server-side Kerberos fixes (#457) ([27ce28dad5](https://github.com/Devolutions/sspi-rs/commit/27ce28dad5aa490d094b4ea1db5a315ea1478264))
|
||||
|
||||
- Kerberos server MIC token generation and validation (#464) ([12fbd706a8](https://github.com/Devolutions/sspi-rs/commit/12fbd706a8e807b4e4ea9b6bb39f4bace60afd9a))
|
||||
|
||||
- Kerberos server WRAP token generation and validation (#463) ([4bbe4071c8](https://github.com/Devolutions/sspi-rs/commit/4bbe4071c80172ee2c85552ef1060a65394a45c0))
|
||||
|
||||
## [[0.15.14](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.15.13...sspi-v0.15.14)] - 2025-07-01
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Update picky-krb to 0.11 (#460) ([5157bee02b](https://github.com/Devolutions/sspi-rs/commit/5157bee02b0383571c726801bda15f0dd9dc7934))
|
||||
|
||||
|
||||
|
||||
## [[0.15.13](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.15.12...sspi-v0.15.13)] - 2025-06-23
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Bump windows-sys from 0.59.0 to 0.60.2 in the windows group across 1 directory (#455) ([5744c8b4b3](https://github.com/Devolutions/sspi-rs/commit/5744c8b4b3aa5a47a2a25e9375434333de769002))
|
||||
|
||||
## [[0.15.12](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.15.11...sspi-v0.15.12)] - 2025-06-20
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Invalid Kerberos token DER encoding (#453) ([0ec3e687dd](https://github.com/Devolutions/sspi-rs/commit/0ec3e687dd28ff95910c79b4781d538cbffb9a80))
|
||||
|
||||
The default behavior of serializing a KrbMessage<T> was accidentally changed.
|
||||
|
||||
## [[0.15.11](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.15.10...sspi-v0.15.11)] - 2025-06-11
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Negotiate attempts KDC detection even when Kerberos is disabled (#447) ([c56132c3f8](https://github.com/Devolutions/sspi-rs/commit/c56132c3f8d3b7e957e64577109158511ee3f4b8))
|
||||
|
||||
## [[0.15.10](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.15.9...sspi-v0.15.10)] - 2025-06-10
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Update picky-krb to 0.10 (#448) ([b8b983d7ae](https://github.com/Devolutions/sspi-rs/commit/b8b983d7aecb8e1c84037d157c2b932668e069b1))
|
||||
|
||||
## [[0.15.9](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.15.8...sspi-v0.15.9)] - 2025-06-05
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Bump windows-registry from 0.4.0 to 0.5.2 in the windows group across 1 directory (#444) ([9a349f7bdc](https://github.com/Devolutions/sspi-rs/commit/9a349f7bdcfe33658af27420af3dd38a88d773ab))
|
||||
|
||||
## [[0.15.8](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.15.7...sspi-v0.15.8)] - 2025-06-05
|
||||
|
||||
### Build
|
||||
|
||||
- Migrate from `winreg` to `windows-registry` crate (#441) ([8631235c8a](https://github.com/Devolutions/sspi-rs/commit/8631235c8a3f93e6f4573142101a25210adb49a5))
|
||||
|
||||
|
||||
|
||||
## [[0.15.7](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.15.6...sspi-v0.15.7)] - 2025-05-29
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Do not log at info-level return values (#438) ([68d02e410d](https://github.com/Devolutions/sspi-rs/commit/68d02e410dadf0278ed2a109117c71c43920ea4f))
|
||||
|
||||
## [[0.15.6](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.15.5...sspi-v0.15.6)] - 2025-05-27
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Lower info-level logs to debug-level (#436) ([665cb1e1dc](https://github.com/Devolutions/sspi-rs/commit/665cb1e1dc148683779e65f66d1408ddaa911bea))
|
||||
|
||||
## [[0.15.5](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.15.4...sspi-v0.15.5)] - 2025-04-25
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- Add `query_context_session_key` (#417) ([862657a57c](https://github.com/Devolutions/sspi-rs/commit/862657a57c781e348ef5ccafe540511b19148b44))
|
||||
|
||||
This addition is an implementation for the SSP API
|
||||
`QueryContextAttributesEx(SECPKG_ATTR_SESSION_KEY)`. It is required for
|
||||
protocols such as SMB, and adding it to the `Sspi` trait, enables access
|
||||
to it across all the SSP packages implemented.
|
||||
|
||||
This adds the option for using Kerberos and Negotiate session keys.
|
||||
|
||||
## [[0.15.4](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.15.3...sspi-v0.15.4)] - 2025-03-24
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Update dependencies
|
||||
|
||||
## [[0.15.3](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.15.2...sspi-v0.15.3)] - 2025-03-12
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Set correct seq number in MIC token (#390) ([69f03c2933](https://github.com/Devolutions/sspi-rs/commit/69f03c2933c1da106b64a565a757666cd4d94bde))
|
||||
|
||||
Fixes Kerberos LDAP auth.
|
||||
The problem was in the invalid sequence number in MIC token.
|
||||
|
||||
- NTLM RPC auth (#395) ([34d896c9ce](https://github.com/Devolutions/sspi-rs/commit/34d896c9cee7e0b3e0d5ee3c5002c781d9fc8fbf))
|
||||
|
||||
## [[0.15.2](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.15.1...sspi-v0.15.2)] - 2025-02-27
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- Support `SECBUFFER_READONLY_WITH_CHECKSUM` flag (#357) ([397fd9502d](https://github.com/Devolutions/sspi-rs/commit/397fd9502dc315e4e8e7c4700b6e789c5e7b44c3))
|
||||
|
||||
- Add `USE_DCE_STYLE` flag support (#358) ([0f78bccaea](https://github.com/Devolutions/sspi-rs/commit/0f78bccaea7ac5620f83de68d3559f212262c789))
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Kerberos authentication and encryption for RPC and RDP (#372) ([442dfc1382](https://github.com/Devolutions/sspi-rs/commit/442dfc1382033f6f81bb4cd021cca7318cce224e))
|
||||
|
||||
## [[0.15.1](https://github.com/Devolutions/sspi-rs/compare/sspi-v0.15.0...sspi-v0.15.1)] - 2025-02-04
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- Add `make_signature` and `verify_signature` to `Sspi` trait (#343) ([040188a34d](https://github.com/Devolutions/sspi-rs/commit/040188a34d5d7b8607825b25a4eb78c25c6b57cc))
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Store session key when using server-side NTLM implementation (#354) ([41d1ca7fed](https://github.com/Devolutions/sspi-rs/commit/41d1ca7fed623759dcc9ff6f28c7558ecfa6fcbd))
|
||||
Vendored
+443
@@ -0,0 +1,443 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2024"
|
||||
rust-version = "1.89"
|
||||
name = "sspi"
|
||||
version = "0.21.3"
|
||||
authors = ["Devolutions Inc. <infos@devolutions.net>"]
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "A Rust implementation of the Security Support Provider Interface (SSPI) API"
|
||||
homepage = "https://github.com/devolutions/sspi-rs"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"ntlm",
|
||||
"auth",
|
||||
"sspi",
|
||||
"windows",
|
||||
"kerberos",
|
||||
]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/devolutions/sspi-rs"
|
||||
resolver = "2"
|
||||
|
||||
[features]
|
||||
__install-crypto-provider = []
|
||||
__rustls-used = ["dep:rustls"]
|
||||
__test-data = []
|
||||
aws-lc-rs = [
|
||||
"rustls?/aws-lc-rs",
|
||||
"__install-crypto-provider",
|
||||
]
|
||||
default = ["aws-lc-rs"]
|
||||
dns_resolver = [
|
||||
"dep:hickory-resolver",
|
||||
"dep:hickory-proto",
|
||||
"dep:tokio",
|
||||
]
|
||||
network_client = [
|
||||
"dep:reqwest",
|
||||
"dep:portpicker",
|
||||
"dep:rustls-native-certs",
|
||||
"__rustls-used",
|
||||
]
|
||||
ring = [
|
||||
"rustls?/ring",
|
||||
"reqwest?/rustls-tls-native-roots",
|
||||
"__install-crypto-provider",
|
||||
]
|
||||
scard = [
|
||||
"dep:winscard",
|
||||
"dep:cryptoki",
|
||||
]
|
||||
tsssp = [
|
||||
"dep:rustls",
|
||||
"__rustls-used",
|
||||
]
|
||||
|
||||
[lib]
|
||||
name = "sspi"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[example]]
|
||||
name = "client"
|
||||
path = "examples/client.rs"
|
||||
|
||||
[[example]]
|
||||
name = "kerberos"
|
||||
path = "examples/kerberos.rs"
|
||||
required-features = ["network_client"]
|
||||
|
||||
[[example]]
|
||||
name = "server"
|
||||
path = "examples/server.rs"
|
||||
|
||||
[[test]]
|
||||
name = "sspi"
|
||||
path = "tests/sspi/main.rs"
|
||||
|
||||
[dependencies.async-recursion]
|
||||
version = "1.1"
|
||||
|
||||
[dependencies.bitflags]
|
||||
version = "2.11"
|
||||
|
||||
[dependencies.bytemuck]
|
||||
version = "1.24"
|
||||
|
||||
[dependencies.byteorder]
|
||||
version = "1.5"
|
||||
|
||||
[dependencies.cfg-if]
|
||||
version = "1"
|
||||
|
||||
[dependencies.crypto-bigint]
|
||||
version = "0.7"
|
||||
features = ["alloc"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.crypto-mac]
|
||||
version = "0.11"
|
||||
|
||||
[dependencies.getrandom]
|
||||
version = "0.3"
|
||||
|
||||
[dependencies.hickory-proto]
|
||||
version = "0.25"
|
||||
optional = true
|
||||
|
||||
[dependencies.hickory-resolver]
|
||||
version = "0.25"
|
||||
optional = true
|
||||
|
||||
[dependencies.hmac]
|
||||
version = "0.13"
|
||||
|
||||
[dependencies.md-5]
|
||||
version = "0.11"
|
||||
|
||||
[dependencies.md4]
|
||||
version = "0.10"
|
||||
|
||||
[dependencies.num-derive]
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.num-traits]
|
||||
version = "0.2"
|
||||
default-features = true
|
||||
|
||||
[dependencies.oid]
|
||||
version = "0.2"
|
||||
|
||||
[dependencies.picky]
|
||||
version = "^7.0.0-rc.25"
|
||||
default-features = false
|
||||
|
||||
[dependencies.picky-asn1]
|
||||
version = "0.10"
|
||||
features = ["time_conversion"]
|
||||
|
||||
[dependencies.picky-asn1-der]
|
||||
version = "0.5"
|
||||
|
||||
[dependencies.picky-asn1-x509]
|
||||
version = "0.15"
|
||||
features = ["pkcs7"]
|
||||
|
||||
[dependencies.picky-krb]
|
||||
version = "0.12"
|
||||
|
||||
[dependencies.portpicker]
|
||||
version = "0.1"
|
||||
optional = true
|
||||
|
||||
[dependencies.rand]
|
||||
version = "0.10"
|
||||
|
||||
[dependencies.rand_core]
|
||||
version = "0.10"
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.12"
|
||||
features = [
|
||||
"blocking",
|
||||
"rustls-tls-no-provider",
|
||||
]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.rsa]
|
||||
version = "^0.10.0-rc.18"
|
||||
default-features = false
|
||||
|
||||
[dependencies.rustls]
|
||||
version = "0.23"
|
||||
features = [
|
||||
"logging",
|
||||
"std",
|
||||
"tls12",
|
||||
]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.rustls-native-certs]
|
||||
version = "0.8"
|
||||
optional = true
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
|
||||
[dependencies.sha1]
|
||||
version = "0.11"
|
||||
default-features = false
|
||||
|
||||
[dependencies.sha2]
|
||||
version = "0.11"
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.52"
|
||||
features = [
|
||||
"time",
|
||||
"rt",
|
||||
"rt-multi-thread",
|
||||
]
|
||||
optional = true
|
||||
|
||||
[dependencies.tracing]
|
||||
version = "0.1"
|
||||
default-features = true
|
||||
|
||||
[dependencies.url]
|
||||
version = "2.5"
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1.23"
|
||||
features = ["v4"]
|
||||
default-features = true
|
||||
|
||||
[dependencies.widestring]
|
||||
version = "1.2"
|
||||
|
||||
[dependencies.winscard]
|
||||
version = "0.3"
|
||||
optional = true
|
||||
|
||||
[dependencies.zeroize]
|
||||
version = "1.8"
|
||||
features = ["zeroize_derive"]
|
||||
|
||||
[dev-dependencies.base64]
|
||||
version = "0.22"
|
||||
|
||||
[dev-dependencies.cfg-if]
|
||||
version = "1"
|
||||
|
||||
[dev-dependencies.picky]
|
||||
version = "^7.0.0-rc.25"
|
||||
features = ["x509"]
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies.proptest]
|
||||
version = "1.6"
|
||||
|
||||
[dev-dependencies.static_assertions]
|
||||
version = "1"
|
||||
|
||||
[dev-dependencies.tracing-subscriber]
|
||||
version = "0.3"
|
||||
features = ["env-filter"]
|
||||
|
||||
[dev-dependencies.whoami]
|
||||
version = "2.1"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.async-dnssd]
|
||||
version = "0.5"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.curve25519-dalek]
|
||||
version = "^5.0.0-rc.1"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.ed25519-dalek]
|
||||
version = "^3.0.0-rc.1"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.futures]
|
||||
version = "0.3"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.p256]
|
||||
version = "^0.14.0-rc.14"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.p384]
|
||||
version = "^0.14.0-rc.14"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.p521]
|
||||
version = "^0.14.0-rc.14"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.pkcs1]
|
||||
version = "^0.8.0-rc.4"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.primeorder]
|
||||
version = "^0.14.0-rc.14"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.rustcrypto-ff]
|
||||
version = "^0.14.0-rc.1"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.rustcrypto-ff_derive]
|
||||
version = "^0.14.0-rc.0"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.rustcrypto-group]
|
||||
version = "^0.14.0-rc.1"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.tokio]
|
||||
version = "1.52"
|
||||
features = [
|
||||
"time",
|
||||
"rt",
|
||||
"rt-multi-thread",
|
||||
]
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.cryptoki]
|
||||
version = "0.12"
|
||||
optional = true
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.time]
|
||||
version = "0.3"
|
||||
features = ["std"]
|
||||
default-features = false
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies.getrandom]
|
||||
version = "0.3"
|
||||
features = ["wasm_js"]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies.time]
|
||||
version = "0.3"
|
||||
features = ["wasm-bindgen"]
|
||||
default-features = false
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies.uuid]
|
||||
version = "1.23"
|
||||
features = ["js"]
|
||||
default-features = false
|
||||
|
||||
[target."cfg(windows)".dependencies.windows]
|
||||
version = "0.62"
|
||||
features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System",
|
||||
"Win32_NetworkManagement_Dns",
|
||||
"Win32_Security_Cryptography",
|
||||
]
|
||||
|
||||
[target."cfg(windows)".dependencies.windows-registry]
|
||||
version = "0.6"
|
||||
|
||||
[lints.clippy]
|
||||
as_pointer_underscore = "warn"
|
||||
as_ptr_cast_mut = "warn"
|
||||
as_underscore = "warn"
|
||||
filetype_is_file = "warn"
|
||||
float_cmp = "warn"
|
||||
float_cmp_const = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
get_unwrap = "warn"
|
||||
lossy_float_literal = "warn"
|
||||
mem_forget = "warn"
|
||||
missing_safety_doc = "warn"
|
||||
mixed_read_write_in_expression = "warn"
|
||||
multiple_unsafe_ops_per_block = "warn"
|
||||
needless_raw_strings = "warn"
|
||||
non_ascii_literal = "warn"
|
||||
panic = "warn"
|
||||
ptr_cast_constness = "warn"
|
||||
rc_mutex = "warn"
|
||||
suspicious_xor_used_as_pow = "warn"
|
||||
undocumented_unsafe_blocks = "warn"
|
||||
unnecessary_safety_comment = "warn"
|
||||
unused_result_ok = "warn"
|
||||
|
||||
[lints.rust]
|
||||
ambiguous_negative_literals = "warn"
|
||||
elided_lifetimes_in_paths = "warn"
|
||||
redundant_lifetimes = "warn"
|
||||
single_use_lifetimes = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
unreachable_pub = "warn"
|
||||
unused_lifetimes = "warn"
|
||||
unused_qualifications = "warn"
|
||||
|
||||
[lints.rust.unexpected_cfgs]
|
||||
level = "warn"
|
||||
priority = 0
|
||||
check-cfg = ["cfg(fuzzing)"]
|
||||
|
||||
[profile.ffi-production]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
split-debuginfo = "packed"
|
||||
panic = "abort"
|
||||
inherits = "release"
|
||||
strip = false
|
||||
|
||||
[profile.ffi-production.package.aes]
|
||||
opt-level = 3
|
||||
|
||||
[profile.ffi-production.package.aes-gcm]
|
||||
opt-level = 3
|
||||
|
||||
[profile.ffi-production.package.aes-kw]
|
||||
opt-level = 3
|
||||
|
||||
[profile.ffi-production.package.argon2]
|
||||
opt-level = 3
|
||||
|
||||
[profile.ffi-production.package.cbc]
|
||||
opt-level = 3
|
||||
|
||||
[profile.ffi-production.package.crypto-bigint]
|
||||
opt-level = 3
|
||||
|
||||
[profile.ffi-production.package.ctr]
|
||||
opt-level = 3
|
||||
|
||||
[profile.ffi-production.package.des]
|
||||
opt-level = 3
|
||||
|
||||
[profile.ffi-production.package.hmac]
|
||||
opt-level = 3
|
||||
|
||||
[profile.ffi-production.package.p256]
|
||||
opt-level = 3
|
||||
|
||||
[profile.ffi-production.package.p384]
|
||||
opt-level = 3
|
||||
|
||||
[profile.ffi-production.package.p521]
|
||||
opt-level = 3
|
||||
|
||||
[profile.ffi-production.package.pbkdf2]
|
||||
opt-level = 3
|
||||
|
||||
[profile.ffi-production.package.rsa]
|
||||
opt-level = 3
|
||||
|
||||
[profile.ffi-production.package.sha1]
|
||||
opt-level = 3
|
||||
|
||||
[profile.ffi-production.package.sha2]
|
||||
opt-level = 3
|
||||
|
||||
[profile.ffi-production.package.sha3]
|
||||
opt-level = 3
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
[package]
|
||||
name = "sspi"
|
||||
version = "0.21.3"
|
||||
edition = "2024"
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
homepage = "https://github.com/devolutions/sspi-rs"
|
||||
repository = "https://github.com/devolutions/sspi-rs"
|
||||
authors = ["Devolutions Inc. <infos@devolutions.net>"]
|
||||
description = "A Rust implementation of the Security Support Provider Interface (SSPI) API"
|
||||
keywords = ["ntlm", "auth", "sspi", "windows", "kerberos"]
|
||||
rust-version = "1.89"
|
||||
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"ffi",
|
||||
"ffi/symbol-rename-macro",
|
||||
"crates/winscard",
|
||||
"crates/ffi-types",
|
||||
"crates/dpapi",
|
||||
"crates/dpapi-core",
|
||||
"crates/dpapi-pdu",
|
||||
"crates/dpapi-transport",
|
||||
"crates/dpapi-native-transport",
|
||||
"crates/dpapi-fuzzing",
|
||||
"crates/dpapi-web",
|
||||
"crates/kdc",
|
||||
]
|
||||
exclude = [
|
||||
"tools/wasm-testcompile",
|
||||
"tools/dpapi-cli-client",
|
||||
]
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
|
||||
|
||||
# == Correctness == #
|
||||
ambiguous_negative_literals = "warn"
|
||||
|
||||
# == Style, readability == #
|
||||
unreachable_pub = "warn"
|
||||
single_use_lifetimes = "warn"
|
||||
elided_lifetimes_in_paths = "warn"
|
||||
unused_lifetimes = "warn"
|
||||
redundant_lifetimes = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
unused_qualifications = "warn"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
|
||||
# == Safer unsafe == #
|
||||
as_ptr_cast_mut = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
ptr_cast_constness = "warn"
|
||||
unnecessary_safety_comment = "warn"
|
||||
missing_safety_doc = "warn"
|
||||
as_pointer_underscore = "warn"
|
||||
undocumented_unsafe_blocks = "warn"
|
||||
multiple_unsafe_ops_per_block = "warn"
|
||||
|
||||
# == Correctness == #
|
||||
as_underscore = "warn"
|
||||
filetype_is_file = "warn"
|
||||
float_cmp_const = "warn"
|
||||
float_cmp = "warn"
|
||||
get_unwrap = "warn"
|
||||
lossy_float_literal = "warn"
|
||||
mem_forget = "warn"
|
||||
mixed_read_write_in_expression = "warn"
|
||||
needless_raw_strings = "warn"
|
||||
non_ascii_literal = "warn"
|
||||
panic = "warn"
|
||||
rc_mutex = "warn"
|
||||
suspicious_xor_used_as_pow = "warn"
|
||||
unused_result_ok = "warn"
|
||||
|
||||
[workspace.dependencies]
|
||||
uuid = { version = "1.23", default-features = false }
|
||||
tracing = { version = "0.1", default-features = false }
|
||||
bitflags = "2.11"
|
||||
rand = "0.10"
|
||||
rand_core = "0.10"
|
||||
getrandom = "0.3"
|
||||
cfg-if = "1"
|
||||
time = { version = "0.3", default-features = false }
|
||||
sha1 = { version = "0.11", default-features = false }
|
||||
sha2 = "0.11"
|
||||
num-derive = "0.4"
|
||||
num-traits = { version = "0.2", default-features = false }
|
||||
|
||||
picky = { version = "=7.0.0-rc.25", default-features = false }
|
||||
picky-asn1 = "0.10"
|
||||
picky-asn1-der = "0.5"
|
||||
picky-asn1-x509 = "0.15"
|
||||
picky-krb = "0.12"
|
||||
|
||||
tokio = "1.52"
|
||||
ffi-types = { path = "crates/ffi-types" }
|
||||
winscard = { version = "0.3", path = "crates/winscard" }
|
||||
|
||||
dpapi = { version = "0.0.0", path = "crates/dpapi" }
|
||||
dpapi-core = { version = "0.1.0", path = "crates/dpapi-core" }
|
||||
dpapi-pdu = { version = "0.1.0", path = "crates/dpapi-pdu" }
|
||||
dpapi-transport = { version = "0.1.0", path = "crates/dpapi-transport" }
|
||||
dpapi-native-transport = { version = "0.1.0", path = "crates/dpapi-native-transport" }
|
||||
|
||||
rsa = { version = "=0.10.0-rc.18", default-features = false }
|
||||
base64 = "0.22"
|
||||
whoami = "2.1"
|
||||
tracing-subscriber = "0.3"
|
||||
proptest = "1.6"
|
||||
serde = "1"
|
||||
byteorder = "1.5"
|
||||
crypto-bigint = { version = "0.7", default-features = false, features = ["alloc"] }
|
||||
hmac = "0.13"
|
||||
|
||||
[features]
|
||||
default = ["aws-lc-rs"]
|
||||
|
||||
# Enable reqwest for HTTP requests.
|
||||
network_client = ["dep:reqwest", "dep:portpicker", "dep:rustls-native-certs", "__rustls-used"]
|
||||
# KDC discovery by querying DNS records.
|
||||
dns_resolver = ["dep:hickory-resolver", "dep:hickory-proto","dep:tokio"]
|
||||
# TSSSP should be used only on Windows as a native CredSSP replacement.
|
||||
tsssp = ["dep:rustls", "__rustls-used"]
|
||||
# Turns on Kerberos smart card logon.
|
||||
scard = ["dep:winscard", "dep:cryptoki"]
|
||||
# Use AWS LC as TLS cryptography provider.
|
||||
aws-lc-rs = ["rustls?/aws-lc-rs", "__install-crypto-provider"]
|
||||
# Use ring as TLS cryptography provider.
|
||||
ring = ["rustls?/ring", "reqwest?/rustls-tls-native-roots", "__install-crypto-provider"]
|
||||
|
||||
# Internal (PRIVATE!) features. Do not rely on these whatsoever. They may disappear at anytime.
|
||||
|
||||
__test-data = []
|
||||
__rustls-used = ["dep:rustls"]
|
||||
__install-crypto-provider = []
|
||||
|
||||
[dependencies]
|
||||
bitflags.workspace = true
|
||||
rand.workspace = true
|
||||
rand_core.workspace = true
|
||||
getrandom.workspace = true
|
||||
cfg-if.workspace = true
|
||||
picky.workspace = true
|
||||
sha1.workspace = true
|
||||
sha2.workspace = true
|
||||
num-derive.workspace = true
|
||||
num-traits = { workspace = true, default-features = true }
|
||||
picky-asn1-der.workspace = true
|
||||
picky-asn1-x509 = { workspace = true, features = ["pkcs7"] }
|
||||
uuid = { workspace = true, features = ["v4"], default-features = true }
|
||||
# `rt-multi-thread` feature is required for `tokio::task::block_in_place` function.
|
||||
tokio = { workspace = true, optional = true, features = ["time", "rt", "rt-multi-thread"] }
|
||||
winscard = { workspace = true, optional = true }
|
||||
rsa = { workspace = true }
|
||||
tracing = { workspace = true, default-features = true }
|
||||
serde.workspace = true
|
||||
picky-krb.workspace = true
|
||||
picky-asn1 = { workspace = true, features = ["time_conversion"] }
|
||||
byteorder.workspace = true
|
||||
crypto-bigint.workspace = true
|
||||
hmac.workspace = true
|
||||
widestring = "1.2"
|
||||
|
||||
url = "2.5"
|
||||
|
||||
md-5 = "0.11"
|
||||
md4 = "0.10"
|
||||
crypto-mac = "0.11"
|
||||
oid = "0.2"
|
||||
|
||||
reqwest = { version = "0.12", optional = true, default-features = false, features = ["blocking", "rustls-tls-no-provider"] }
|
||||
hickory-resolver = { version = "0.25", optional = true }
|
||||
hickory-proto = { version = "0.25", optional = true }
|
||||
portpicker = { version = "0.1", optional = true }
|
||||
rustls = { version = "0.23", optional = true, default-features = false, features = ["logging", "std", "tls12"] }
|
||||
rustls-native-certs = { version = "0.8", optional = true }
|
||||
zeroize = { version = "1.8", features = ["zeroize_derive"] }
|
||||
async-recursion = "1.1"
|
||||
bytemuck = "1.24"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-registry = "0.6"
|
||||
windows = { version = "0.62", features = [ "Win32_Foundation", "Win32_System", "Win32_NetworkManagement_Dns", "Win32_Security_Cryptography"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
uuid = { workspace = true, features = ["js"]}
|
||||
getrandom = { workspace = true, features = ["wasm_js"] }
|
||||
time = { workspace = true, features = ["wasm-bindgen"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
cryptoki = { version = "0.12", optional = true }
|
||||
time = { workspace = true, features = ["std"] }
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
|
||||
async-dnssd = "0.5"
|
||||
futures = "0.3"
|
||||
tokio = { workspace = true, features = ["time", "rt", "rt-multi-thread"] }
|
||||
|
||||
# Pin transitive dependencies
|
||||
# TODO: Remove when stable versions will be released
|
||||
# RustCrypto crates
|
||||
pkcs1 = "=0.8.0-rc.4"
|
||||
p256 = "=0.14.0-rc.14"
|
||||
primeorder = "=0.14.0-rc.14"
|
||||
p384 = "=0.14.0-rc.14"
|
||||
p521 = "=0.14.0-rc.14"
|
||||
|
||||
# "Zero-knowledge Cryptography in Rust" crates
|
||||
rustcrypto-ff = "=0.14.0-rc.1"
|
||||
rustcrypto-ff_derive = "=0.14.0-rc.0"
|
||||
rustcrypto-group = "=0.14.0-rc.1"
|
||||
|
||||
# "dalek cryptography" crates
|
||||
ed25519-dalek = "=3.0.0-rc.1"
|
||||
curve25519-dalek = "=5.0.0-rc.1"
|
||||
|
||||
[dev-dependencies]
|
||||
base64.workspace = true
|
||||
static_assertions = "1"
|
||||
whoami.workspace = true
|
||||
picky = { workspace = true, features = ["x509"] }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
proptest.workspace = true
|
||||
cfg-if.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[example]]
|
||||
name = "kerberos"
|
||||
required-features = ["network_client"]
|
||||
|
||||
[profile.ffi-production]
|
||||
inherits = "release"
|
||||
strip = false
|
||||
split-debuginfo = "packed"
|
||||
lto = true
|
||||
opt-level = "z"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
# Optimize for speed the cryptographic libraries.
|
||||
[profile.ffi-production.package.crypto-bigint]
|
||||
opt-level = 3
|
||||
[profile.ffi-production.package.p256]
|
||||
opt-level = 3
|
||||
[profile.ffi-production.package.p384]
|
||||
opt-level = 3
|
||||
[profile.ffi-production.package.p521]
|
||||
opt-level = 3
|
||||
[profile.ffi-production.package.rsa]
|
||||
opt-level = 3
|
||||
[profile.ffi-production.package.sha1]
|
||||
opt-level = 3
|
||||
[profile.ffi-production.package.sha2]
|
||||
opt-level = 3
|
||||
[profile.ffi-production.package.sha3]
|
||||
opt-level = 3
|
||||
[profile.ffi-production.package.aes-gcm]
|
||||
opt-level = 3
|
||||
[profile.ffi-production.package.aes]
|
||||
opt-level = 3
|
||||
[profile.ffi-production.package.aes-kw]
|
||||
opt-level = 3
|
||||
[profile.ffi-production.package.argon2]
|
||||
opt-level = 3
|
||||
[profile.ffi-production.package.ctr]
|
||||
opt-level = 3
|
||||
[profile.ffi-production.package.cbc]
|
||||
opt-level = 3
|
||||
[profile.ffi-production.package.des]
|
||||
opt-level = 3
|
||||
[profile.ffi-production.package.pbkdf2]
|
||||
opt-level = 3
|
||||
[profile.ffi-production.package.hmac]
|
||||
opt-level = 3
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string></string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string></string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string></string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0.0</string>
|
||||
<key>CSResourcesFileMapped</key>
|
||||
<true/>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
Vendored
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
Vendored
+102
@@ -0,0 +1,102 @@
|
||||
# sspi-rs
|
||||
|
||||
[](https://docs.rs/sspi/) [](https://crates.io/crates/sspi)
|
||||
|
||||
**sspi-rs** is a Rust implementation of [Security Support Provider Interface (SSPI)](https://docs.microsoft.com/en-us/windows/win32/rpc/security-support-provider-interface-sspi-). It ships with platform-independent implementations of [Security Support Providers (SSP)](https://docs.microsoft.com/en-us/windows/win32/rpc/security-support-providers-ssps-), and is able to utilize native Microsoft libraries when ran under Windows.
|
||||
|
||||
The purpose of sspi-rs is to clean the original interface from cluttering and provide users with Rust-friendly SSPs for execution under *nix or any other platform that is able to compile Rust.
|
||||
|
||||
## Overview
|
||||
|
||||
The sspi-rs works in accordance with the MSDN documentation. At the moment, [NT LAN Manager (NTLM)](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4) is implemented and available for platform independent execution. It is also possible to create your own SSPs by implementing the [`SspiImpl`]() trait.
|
||||
|
||||
###### Ease of use
|
||||
|
||||
Some SSPI functions tend to be cumbersome, that's why sspi-rs allows to use SSPI in a convenient way by utilizing builders.
|
||||
|
||||
## Example
|
||||
|
||||
The usage of the SSPs is as simple as creating an instance of the security provider and calling its functions.
|
||||
|
||||
Here is an example of acquiring a credentials handle and a timestamp of their validity:
|
||||
```rust
|
||||
use sspi::{CredentialUse, Ntlm, Sspi, Username, builders::EmptyInitializeSecurityContext, SecurityBuffer, ClientRequestFlags, DataRepresentation, BufferType, SspiImpl};
|
||||
|
||||
fn main() {
|
||||
let account_name = "example_user";
|
||||
let computer_name = "example_computer";
|
||||
let mut ntlm = Ntlm::new();
|
||||
let username = Username::new(&account_name, Some(&computer_name)).unwrap();
|
||||
let identity = sspi::AuthIdentity {
|
||||
username,
|
||||
password: String::from("example_password").into(),
|
||||
};
|
||||
|
||||
let mut acq_cred_result = ntlm
|
||||
.acquire_credentials_handle()
|
||||
.with_credential_use(CredentialUse::Outbound)
|
||||
.with_auth_data(&identity)
|
||||
.execute()
|
||||
.unwrap();
|
||||
|
||||
let mut output_buffer = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
|
||||
// first time calling initialize_security_context, the input buffer should be empty
|
||||
let mut input_buffer = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
|
||||
|
||||
// create a builder for the first call to initialize_security_context
|
||||
// the target should start with the protocol name, e.g. "HTTP/example.com" or "LDAP/example.com"
|
||||
let mut builder = EmptyInitializeSecurityContext::<<Ntlm as SspiImpl>::CredentialsHandle>::new()
|
||||
.with_credentials_handle(&mut acq_cred_result.credentials_handle)
|
||||
.with_context_requirements(ClientRequestFlags::CONFIDENTIALITY | ClientRequestFlags::ALLOCATE_MEMORY)
|
||||
.with_target_data_representation(DataRepresentation::Native)
|
||||
.with_target_name("LDAP/example.com")
|
||||
.with_input(&mut input_buffer)
|
||||
.with_output(&mut output_buffer);
|
||||
|
||||
// call initialize_security_context
|
||||
// Note: the initialize_security_context_impl returns a generator, for NTLM,
|
||||
// this generator will never yield as NTLM requires no network communication to a third party
|
||||
// but negotiate and kerberos do require network communication, so the generator is used to
|
||||
// allow the caller to provide the network information through the generator.resume() method
|
||||
// take a look at the examples/kerberos.rs for more information
|
||||
let _result = ntlm
|
||||
.initialize_security_context_impl(&mut builder)
|
||||
.resolve_to_result()
|
||||
.unwrap();
|
||||
// ... exchange your token in output buffer with the server and repeat the process until either server is satisfied or an error is thrown
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Example of acquiring an SSP provided by Windows:
|
||||
```Rust
|
||||
let mut negotiate = SecurityPackage::from_package_type(
|
||||
SecurityPackageType::Other(String::from("Negotiate"))
|
||||
);
|
||||
```
|
||||
|
||||
## Projects using sspi-rs
|
||||
|
||||
* [Devolutions Gateway](https://github.com/Devolutions/devolutions-gateway)
|
||||
* [IronRDP](https://github.com/Devolutions/IronRDP)
|
||||
* [Python SSPI Library](https://github.com/jborean93/sspilib)
|
||||
* [NetExec](https://github.com/Pennyw0rth/NetExec)
|
||||
* [LDAP client library](https://github.com/kanidm/ldap3/blob/master/proto/examples/sasltest/main.rs)
|
||||
* [Remote Desktop Manager](https://devolutions.net/remote-desktop-manager/)
|
||||
|
||||
(Feel free to open a PR if you know about other projects!)
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of:
|
||||
|
||||
* [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
* [MIT license](http://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
|
||||
dual licensed as above, without any additional terms or conditions.
|
||||
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
# Configuration file for git-cliff
|
||||
|
||||
[changelog]
|
||||
trim = false
|
||||
|
||||
header = """
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
"""
|
||||
|
||||
# https://tera.netlify.app/docs/#introduction
|
||||
body = """
|
||||
{% if version -%}
|
||||
## [[{{ version | trim_start_matches(pat="v") }}]{%- if release_link -%}({{ release_link }}){% endif %}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{%- else -%}
|
||||
## [Unreleased]
|
||||
{%- endif %}
|
||||
|
||||
{% for group, commits in commits | group_by(attribute="group") -%}
|
||||
|
||||
### {{ group | upper_first }}
|
||||
|
||||
{%- for commit in commits %}
|
||||
|
||||
{%- set message = commit.message | upper_first %}
|
||||
|
||||
{%- if commit.breaking %}
|
||||
{%- set breaking = "[**breaking**] " %}
|
||||
{%- else %}
|
||||
{%- set breaking = "" %}
|
||||
{%- endif %}
|
||||
|
||||
{%- set short_sha = commit.id | truncate(length=10, end="") %}
|
||||
{%- set commit_url = "https://github.com/Devolutions/sspi-rs/commit/" ~ commit.id %}
|
||||
{%- set commit_link = "[" ~ short_sha ~ "](" ~ commit_url ~ ")" %}
|
||||
|
||||
- {{ breaking }}{{ message }} ({{ commit_link }}) \
|
||||
{% if commit.body %}\n\n {{ commit.body | replace(from="\n", to="\n ") }}{% endif %}
|
||||
{%- endfor %}
|
||||
|
||||
{% endfor -%}
|
||||
"""
|
||||
|
||||
footer = ""
|
||||
|
||||
[git]
|
||||
conventional_commits = true
|
||||
filter_unconventional = false
|
||||
filter_commits = false
|
||||
date_order = false
|
||||
protect_breaking_commits = true
|
||||
sort_commits = "oldest"
|
||||
|
||||
commit_preprocessors = [
|
||||
# Replace issue number with the link
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/Devolutions/sspi-rs/issues/${2}))"},
|
||||
# Replace commit sha1 with the link
|
||||
{ pattern = '([a-f0-9]{10})([a-f0-9]{30})', replace = "[${0}](https://github.com/Devolutions/sspi-rs/commit/${1}${2})" }
|
||||
]
|
||||
|
||||
# regex for parsing and grouping commits
|
||||
# <!-- <NUMBER> --> is a trick to control the section order: https://github.com/orhun/git-cliff/issues/9#issuecomment-914521594
|
||||
commit_parsers = [
|
||||
{ message = "^chore", skip = true },
|
||||
{ message = "^style", skip = true },
|
||||
{ message = "^refactor", skip = true },
|
||||
{ message = "^test", skip = true },
|
||||
{ message = "^ci", skip = true },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ footer = "^[Cc]hangelog: ?ignore", skip = true },
|
||||
|
||||
{ message = "(?i)security", group = "<!-- 0 -->Security" },
|
||||
{ body = "(?i)security", group = "<!-- 0 -->Security" },
|
||||
{ footer = "^[Ss]ecurity: ?yes", group = "<!-- 0 -->Security" },
|
||||
|
||||
{ message = "^feat", group = "<!-- 1 -->Features" },
|
||||
|
||||
{ message = "^revert", group = "<!-- 3 -->Revert" },
|
||||
{ message = "^fix", group = "<!-- 4 -->Bug Fixes" },
|
||||
{ message = "^perf", group = "<!-- 5 -->Performance" },
|
||||
{ message = "^doc", group = "<!-- 6 -->Documentation" },
|
||||
{ message = "^build", group = "<!-- 7 -->Build" },
|
||||
|
||||
{ message = "(?i)improve", group = "<!-- 2 -->Improvements" },
|
||||
{ message = "(?i)adjust", group = "<!-- 2 -->Improvements" },
|
||||
{ message = "(?i)change", group = "<!-- 2 -->Improvements" },
|
||||
|
||||
{ message = ".*", group = "<!-- 99 -->Please Sort" },
|
||||
]
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
msrv = "1.89"
|
||||
allow-panic-in-tests = true
|
||||
Vendored
+179
@@ -0,0 +1,179 @@
|
||||
// Based on the SSPI client example from MSDN: https://docs.microsoft.com/en-us/windows/win32/secauthn/using-sspi-with-a-windows-sockets-client
|
||||
|
||||
// This example works with the server example using SSPI. The client and server examples are designed to work together.
|
||||
// This example demonstrates initializing an authenticating SSPI session with the NTLM, connecting with a server, establishing
|
||||
// a secure communication session, receiving and decrypting a message from the server within the secure session.
|
||||
|
||||
use std::io;
|
||||
use std::net::TcpStream;
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use sspi::{
|
||||
AuthIdentity, BufferType, ClientRequestFlags, CredentialUse, DataRepresentation, Ntlm, SecurityBuffer,
|
||||
SecurityBufferRef, SecurityStatus, Sspi, SspiImpl, Username,
|
||||
};
|
||||
|
||||
const IP: &str = "127.0.0.1:8080";
|
||||
|
||||
fn main() -> Result<(), io::Error> {
|
||||
let mut stream = TcpStream::connect(IP).expect("Failed to connect to the server");
|
||||
|
||||
println!("Connected to the server.");
|
||||
|
||||
let mut ntlm = Ntlm::new();
|
||||
|
||||
let account_name = whoami::username().unwrap();
|
||||
let computer_name = whoami::hostname().unwrap();
|
||||
let username = Username::new(&account_name, Some(&computer_name)).map_err(io::Error::other)?;
|
||||
|
||||
let identity = AuthIdentity {
|
||||
username,
|
||||
password: String::from("password").into(),
|
||||
};
|
||||
|
||||
do_authentication(&mut ntlm, &identity, &mut stream).expect("Failed to authenticate connection");
|
||||
println!("Authenticated!");
|
||||
|
||||
let mut trailer = Vec::new();
|
||||
|
||||
// By agreement, the server encrypted the message and set the size
|
||||
// of the trailer block to be just what it needed. decrypt_message
|
||||
// needs the size of the trailer block.
|
||||
//
|
||||
// By agreement, the server placed the trailer at the beginning
|
||||
// of the message, and the data comes after the trailer.
|
||||
println!("Receiving the trailer...");
|
||||
read_message(&mut stream, &mut trailer)?;
|
||||
|
||||
let mut data = Vec::new();
|
||||
println!("Receiving the data...");
|
||||
read_message(&mut stream, &mut data)?;
|
||||
|
||||
println!("Encrypted message: {data:?}");
|
||||
|
||||
let mut msg_buffer = vec![
|
||||
SecurityBufferRef::token_buf(&mut trailer),
|
||||
SecurityBufferRef::data_buf(&mut data),
|
||||
];
|
||||
|
||||
let _decryption_flags = ntlm.decrypt_message(&mut msg_buffer)?;
|
||||
|
||||
println!("Decrypting...");
|
||||
println!(
|
||||
"Decrypted message: [{}]",
|
||||
std::str::from_utf8(msg_buffer[1].data()).unwrap()
|
||||
);
|
||||
|
||||
println!("Communication successfully finished.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_authentication(ntlm: &mut Ntlm, identity: &AuthIdentity, mut stream: &mut TcpStream) -> Result<(), io::Error> {
|
||||
let mut acq_cred_result = ntlm
|
||||
.acquire_credentials_handle()
|
||||
.with_credential_use(CredentialUse::Outbound)
|
||||
.with_auth_data(identity)
|
||||
.execute(ntlm)?;
|
||||
|
||||
let mut output_buffer = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
|
||||
let username = whoami::username().unwrap();
|
||||
|
||||
let mut builder = ntlm
|
||||
.initialize_security_context()
|
||||
.with_credentials_handle(&mut acq_cred_result.credentials_handle)
|
||||
.with_context_requirements(ClientRequestFlags::CONFIDENTIALITY | ClientRequestFlags::ALLOCATE_MEMORY)
|
||||
.with_target_data_representation(DataRepresentation::Native)
|
||||
.with_target_name(username.as_str())
|
||||
.with_output(&mut output_buffer);
|
||||
|
||||
let _result = ntlm
|
||||
.initialize_security_context_impl(&mut builder)?
|
||||
.resolve_to_result()?;
|
||||
|
||||
write_message(&mut stream, &output_buffer[0].buffer)?;
|
||||
|
||||
let mut input_buffer = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
|
||||
|
||||
loop {
|
||||
output_buffer[0].buffer.clear();
|
||||
|
||||
read_message(&mut stream, &mut input_buffer[0].buffer)?;
|
||||
|
||||
let mut builder = ntlm
|
||||
.initialize_security_context()
|
||||
.with_credentials_handle(&mut acq_cred_result.credentials_handle)
|
||||
.with_context_requirements(ClientRequestFlags::CONFIDENTIALITY | ClientRequestFlags::ALLOCATE_MEMORY)
|
||||
.with_target_data_representation(DataRepresentation::Native)
|
||||
.with_target_name(username.as_str())
|
||||
.with_input(&mut input_buffer)
|
||||
.with_output(&mut output_buffer);
|
||||
|
||||
let result = ntlm
|
||||
.initialize_security_context_impl(&mut builder)?
|
||||
.resolve_to_result()?;
|
||||
|
||||
if [SecurityStatus::CompleteAndContinue, SecurityStatus::CompleteNeeded].contains(&result.status) {
|
||||
println!("Completing the token...");
|
||||
ntlm.complete_auth_token(&mut output_buffer)?;
|
||||
}
|
||||
|
||||
write_message(&mut stream, &output_buffer[0].buffer)?;
|
||||
|
||||
input_buffer[0].buffer.clear();
|
||||
|
||||
if ![SecurityStatus::CompleteAndContinue, SecurityStatus::ContinueNeeded].contains(&result.status) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// By agreement, the message length is read from the first 2 bytes of the message in little-endian order.
|
||||
pub fn read_message<T: io::Read, A: io::Write>(stream: &mut T, output_buffer: &mut A) -> Result<(), io::Error> {
|
||||
let msg_len = stream.read_u16::<LittleEndian>()?;
|
||||
|
||||
let mut buff = vec![0u8; msg_len as usize];
|
||||
stream.read_exact(&mut buff)?;
|
||||
|
||||
output_buffer.write_all(&buff)?;
|
||||
|
||||
println!("Received the buffer [{} bytes]: {:?}", buff.len(), buff);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// By agreement, the message length is written in the first 2 bytes of the message in little-endian order.
|
||||
pub fn write_message<T: io::Write>(stream: &mut T, input_buffer: &[u8]) -> Result<(), io::Error> {
|
||||
if !input_buffer.is_empty() {
|
||||
println!("Sending the buffer [{} bytes]: {:?}", input_buffer.len(), input_buffer);
|
||||
|
||||
stream.write_u16::<LittleEndian>(input_buffer.len() as u16)?;
|
||||
stream.write_all(input_buffer)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_read_correctly() {
|
||||
let mut msg = vec![0x3, 0x0];
|
||||
msg.append(&mut b"abc".to_vec());
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
read_message(&mut msg.as_slice(), &mut buffer).unwrap();
|
||||
|
||||
assert_eq!(buffer, b"abc".to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_written_correctly() {
|
||||
let mut msg = vec![9, 8, 7];
|
||||
let mut stream = Vec::new();
|
||||
|
||||
write_message(&mut stream, &mut msg).unwrap();
|
||||
|
||||
assert_eq!(stream, vec![3, 0, 9, 8, 7]);
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
use std::error::Error;
|
||||
|
||||
use base64::Engine;
|
||||
use reqwest::StatusCode;
|
||||
use reqwest::header::{
|
||||
ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CONNECTION, CONTENT_LENGTH, HOST, USER_AGENT,
|
||||
WWW_AUTHENTICATE,
|
||||
};
|
||||
use sspi::{
|
||||
AcquireCredentialsHandleResult, BufferType, ClientRequestFlags, CredentialsBuffers, DataRepresentation,
|
||||
InitializeSecurityContextResult, Kerberos, KerberosConfig, SecurityBuffer, SecurityStatus, Sspi, SspiImpl,
|
||||
Username,
|
||||
};
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{EnvFilter, fmt};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let kdc_url = std::env::var("SSPI_KDC_URL").expect("missing KDC URL set in SSPI_KDC_URL"); //tcp://ad-compter-name.domain:88
|
||||
let hostname = std::env::var("SSPI_WINRM_HOST").expect("missing host name set in SSPI_WINRM_HOST"); // winrm_server_name.domain
|
||||
let username = std::env::var("SSPI_WINRM_USER").expect("missing username set in SSPI_WINRM_USER"); // username@domain
|
||||
let password = std::env::var("SSPI_WINRM_PASS").expect("missing password set in SSPI_WINRM_PASS");
|
||||
let auth_method = std::env::var("SSPI_WINRM_AUTH").expect("missing auth METHOD set in SSPI_WINRM_AUTH"); // Negotiate or Kerberos
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(fmt::layer())
|
||||
.with(EnvFilter::from_env("SSPI_LOG_LEVEL"))
|
||||
.init();
|
||||
|
||||
let kerberos_config = KerberosConfig::new(&kdc_url, hostname.clone());
|
||||
let mut kerberos = Kerberos::new_client_from_config(kerberos_config).unwrap();
|
||||
|
||||
let mut acq_creds_handle_result = get_cred_handle(&mut kerberos, username, password);
|
||||
|
||||
let mut input_token = String::new();
|
||||
let mut client = reqwest::blocking::Client::new(); // super IMPORTANT, KEEP-ALIVE the http connection!
|
||||
loop {
|
||||
let (output_token, status) = step(
|
||||
&mut kerberos,
|
||||
&mut acq_creds_handle_result.credentials_handle,
|
||||
&input_token,
|
||||
&hostname,
|
||||
)?;
|
||||
|
||||
if status == SecurityStatus::ContinueNeeded || status == SecurityStatus::Ok {
|
||||
let (token_from_server, status_code) =
|
||||
process_authentication(&output_token, &mut client, &auth_method, &hostname)?;
|
||||
if status_code == StatusCode::OK {
|
||||
println!("authenticated");
|
||||
break Ok(());
|
||||
}
|
||||
input_token = token_from_server;
|
||||
} else {
|
||||
return Err("Having problem continue authentication".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_cred_handle(
|
||||
kerberos: &mut Kerberos,
|
||||
username: String,
|
||||
password: String,
|
||||
) -> AcquireCredentialsHandleResult<Option<CredentialsBuffers>> {
|
||||
let identity = sspi::AuthIdentity {
|
||||
username: Username::parse(&username).expect("username is not in the correct format"),
|
||||
password: password.into(),
|
||||
};
|
||||
|
||||
kerberos
|
||||
.acquire_credentials_handle()
|
||||
.with_credential_use(sspi::CredentialUse::Outbound)
|
||||
.with_auth_data(&identity.into())
|
||||
.execute(kerberos)
|
||||
.expect("AcquireCredentialsHandle resulted in error")
|
||||
}
|
||||
|
||||
pub(crate) fn process_authentication(
|
||||
token_neeeds_to_be_sent: &String,
|
||||
client: &mut reqwest::blocking::Client,
|
||||
auth_method: &str,
|
||||
hostname: &str,
|
||||
) -> Result<(String, StatusCode), Box<dyn Error + Send + Sync>> {
|
||||
let server_result = send_http(token_neeeds_to_be_sent, client, hostname, auth_method)?;
|
||||
if server_result.status() == StatusCode::OK {
|
||||
return Ok((String::new(), StatusCode::OK));
|
||||
}
|
||||
let www_authenticate = server_result
|
||||
.headers()
|
||||
.get(WWW_AUTHENTICATE)
|
||||
.ok_or("expecting www-authentication header from server but not found")?;
|
||||
let server_token = www_authenticate
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace(format!("{auth_method} ").as_str(), "");
|
||||
Ok((server_token, server_result.status()))
|
||||
}
|
||||
|
||||
pub(crate) fn send_http(
|
||||
negotiate_token: &String,
|
||||
client: &mut reqwest::blocking::Client,
|
||||
hostname: &str,
|
||||
auth_method: &str,
|
||||
) -> Result<reqwest::blocking::Response, Box<dyn Error + Send + Sync>> {
|
||||
let resp = client
|
||||
.post(format!("http://{hostname}:5985/wsman?PSVersion=7.3.8"))
|
||||
.header(AUTHORIZATION, format!("{auth_method} {negotiate_token}"))
|
||||
.header(HOST, format!("{hostname}:5985"))
|
||||
.header(CONNECTION, "keep-alive")
|
||||
.header(CONTENT_LENGTH, "0")
|
||||
.header(
|
||||
USER_AGENT,
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0",
|
||||
)
|
||||
.header(ACCEPT, "*/*")
|
||||
.header(ACCEPT_ENCODING, "gzip, deflate")
|
||||
.header(ACCEPT_LANGUAGE, "en-US,en;q=0.9")
|
||||
.send()?;
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
fn step_helper(
|
||||
kerberos: &mut Kerberos,
|
||||
cred_handle: &mut <Kerberos as SspiImpl>::CredentialsHandle,
|
||||
input_buffer: &mut [SecurityBuffer],
|
||||
output_buffer: &mut [SecurityBuffer],
|
||||
hostname: &str,
|
||||
) -> Result<InitializeSecurityContextResult, Box<dyn Error + Send + Sync>> {
|
||||
let target_name = format!("HTTP/{hostname}");
|
||||
let mut builder = kerberos
|
||||
.initialize_security_context()
|
||||
.with_credentials_handle(cred_handle)
|
||||
.with_context_requirements(ClientRequestFlags::MUTUAL_AUTH)
|
||||
.with_target_data_representation(DataRepresentation::Native)
|
||||
.with_target_name(&target_name)
|
||||
.with_input(input_buffer)
|
||||
.with_output(output_buffer);
|
||||
|
||||
let result = kerberos
|
||||
.initialize_security_context_impl(&mut builder)?
|
||||
.resolve_with_default_network_client()?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn step(
|
||||
kerberos: &mut Kerberos,
|
||||
cred_handle: &mut <Kerberos as SspiImpl>::CredentialsHandle,
|
||||
input_token: &String,
|
||||
hostname: &str,
|
||||
) -> Result<(String, SecurityStatus), Box<dyn Error + Send + Sync>> {
|
||||
let input_buffer = base64::engine::general_purpose::STANDARD.decode(input_token).unwrap();
|
||||
let mut secure_input_buffer = vec![SecurityBuffer::new(input_buffer, BufferType::Token)];
|
||||
let mut secure_output_buffer = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
|
||||
|
||||
let result = step_helper(
|
||||
kerberos,
|
||||
cred_handle,
|
||||
&mut secure_input_buffer,
|
||||
&mut secure_output_buffer,
|
||||
hostname,
|
||||
)?;
|
||||
|
||||
let output_buffer = secure_output_buffer[0].to_owned();
|
||||
|
||||
Ok((
|
||||
base64::engine::general_purpose::STANDARD.encode(output_buffer.buffer),
|
||||
result.status,
|
||||
))
|
||||
}
|
||||
Vendored
+159
@@ -0,0 +1,159 @@
|
||||
// Based on the SSPI server example from MSDN: https://docs.microsoft.com/en-us/windows/win32/secauthn/using-sspi-with-a-windows-sockets-server
|
||||
|
||||
// This example works with the client example using SSPI.
|
||||
// It demonstrates how to connect with a client, establish a secure communication session, and send the client an encrypted message.
|
||||
|
||||
use std::io;
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use sspi::{
|
||||
AuthIdentity, BufferType, CredentialUse, DataRepresentation, EncryptionFlags, Ntlm, SecurityBuffer,
|
||||
SecurityBufferRef, SecurityStatus, ServerRequestFlags, Sspi, SspiImpl, Username,
|
||||
};
|
||||
|
||||
const IP: &str = "127.0.0.1:8080";
|
||||
|
||||
fn main() -> Result<(), io::Error> {
|
||||
let listener = TcpListener::bind(IP).expect("Unable to start the server");
|
||||
|
||||
println!("Started the server.");
|
||||
|
||||
let (mut stream, _client_addr) = listener.accept()?;
|
||||
|
||||
let mut ntlm = Ntlm::new();
|
||||
|
||||
let account_name = whoami::username().unwrap();
|
||||
let computer_name = whoami::hostname().unwrap();
|
||||
let username = Username::new(&account_name, Some(&computer_name)).map_err(io::Error::other)?;
|
||||
|
||||
let identity = AuthIdentity {
|
||||
username,
|
||||
password: String::from("password").into(),
|
||||
};
|
||||
|
||||
do_authentication(&mut ntlm, &identity, &mut stream).expect("Failed to authenticate connection");
|
||||
println!("Authenticated!");
|
||||
|
||||
println!("Sending the encrypted message...");
|
||||
|
||||
let msg = "This is your server speaking!".to_string();
|
||||
|
||||
// By agreement, the server encrypts and sets the size of the
|
||||
// trailer block to be just what it needed. decrypt_message
|
||||
// needs the size of the trailer block.
|
||||
//
|
||||
// By agreement, the server places the trailer at the beginning
|
||||
// of the message, and the data comes after the trailer.
|
||||
let mut token = vec![0u8; ntlm.query_context_sizes()?.security_trailer as usize];
|
||||
let mut data = msg.as_bytes().to_vec();
|
||||
let mut msg_buffer = vec![
|
||||
SecurityBufferRef::token_buf(token.as_mut_slice()),
|
||||
SecurityBufferRef::data_buf(data.as_mut_slice()),
|
||||
];
|
||||
|
||||
println!("Unencrypted message: [{msg}]");
|
||||
println!("Encrypting...");
|
||||
|
||||
let _result = ntlm.encrypt_message(EncryptionFlags::empty(), &mut msg_buffer)?;
|
||||
|
||||
println!("Encrypted message: {:?}", msg_buffer[1].data());
|
||||
|
||||
println!("Sending the trailer...");
|
||||
write_message(&mut stream, msg_buffer[0].data())?;
|
||||
|
||||
println!("Sending the data...");
|
||||
write_message(&mut stream, msg_buffer[1].data())?;
|
||||
|
||||
println!("Communication successfully finished.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_authentication(ntlm: &mut Ntlm, identity: &AuthIdentity, mut stream: &mut TcpStream) -> Result<(), sspi::Error> {
|
||||
let mut acq_cred_result = ntlm
|
||||
.acquire_credentials_handle()
|
||||
.with_credential_use(CredentialUse::Inbound)
|
||||
.with_auth_data(identity)
|
||||
.execute(ntlm)?;
|
||||
|
||||
let mut input_buffer = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
|
||||
let mut output_buffer = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
|
||||
|
||||
loop {
|
||||
read_message(&mut stream, &mut input_buffer[0].buffer)?;
|
||||
|
||||
let builder = ntlm
|
||||
.accept_security_context()
|
||||
.with_credentials_handle(&mut acq_cred_result.credentials_handle)
|
||||
.with_context_requirements(ServerRequestFlags::ALLOCATE_MEMORY)
|
||||
.with_target_data_representation(DataRepresentation::Native)
|
||||
.with_input(&mut input_buffer)
|
||||
.with_output(&mut output_buffer);
|
||||
let result = ntlm.accept_security_context_impl(builder)?.resolve_to_result()?;
|
||||
|
||||
if [SecurityStatus::CompleteAndContinue, SecurityStatus::CompleteNeeded].contains(&result.status) {
|
||||
println!("Completing the token...");
|
||||
ntlm.complete_auth_token(&mut output_buffer)?;
|
||||
}
|
||||
|
||||
write_message(&mut stream, &output_buffer[0].buffer)?;
|
||||
|
||||
output_buffer[0].buffer.clear();
|
||||
input_buffer[0].buffer.clear();
|
||||
|
||||
if ![SecurityStatus::CompleteAndContinue, SecurityStatus::ContinueNeeded].contains(&result.status) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// By agreement, the message length is read from the first 2 bytes of the message in little-endian order.
|
||||
pub fn read_message<T: io::Read, A: io::Write>(stream: &mut T, output_buffer: &mut A) -> Result<(), io::Error> {
|
||||
let msg_len = stream.read_u16::<LittleEndian>()?;
|
||||
|
||||
let mut buff = vec![0u8; msg_len as usize];
|
||||
stream.read_exact(&mut buff)?;
|
||||
|
||||
output_buffer.write_all(&buff)?;
|
||||
|
||||
println!("Received the buffer [{} bytes]: {:?}", buff.len(), buff);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// By agreement, the message length is written in the first 2 bytes of the message in little-endian order.
|
||||
pub fn write_message<T: io::Write>(stream: &mut T, input_buffer: &[u8]) -> Result<(), io::Error> {
|
||||
if !input_buffer.is_empty() {
|
||||
println!("Sending the buffer [{} bytes]: {:?}", input_buffer.len(), input_buffer);
|
||||
|
||||
stream.write_u16::<LittleEndian>(input_buffer.len() as u16)?;
|
||||
stream.write_all(input_buffer)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_read_correctly() {
|
||||
let mut msg = vec![0x3, 0x0];
|
||||
msg.append(&mut b"abc".to_vec());
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
read_message(&mut msg.as_slice(), &mut buffer).unwrap();
|
||||
|
||||
assert_eq!(buffer, b"abc".to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_written_correctly() {
|
||||
let mut msg = vec![9, 8, 7];
|
||||
let mut stream = Vec::new();
|
||||
|
||||
write_message(&mut stream, &mut msg).unwrap();
|
||||
|
||||
assert_eq!(stream, vec![3, 0, 9, 8, 7]);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc 22a32450b3cada4bb46952cb5109e3b305e8c756d58d37136a195c3bc3dee4cb # shrinks to account_name = "@", domain_name = "A"
|
||||
cc 7f7cdbd18aad0a1d970b6ff9ce7fbfaa7e4dabeb0b55cca0c7f7610f8f0e3cbf # shrinks to value = "A@\\.a"
|
||||
cc f946f1001aacc60322cba7ba063fa386f4e0c6d9497d8144e300f8b4652e378e # shrinks to value = "A\\a\\a"
|
||||
cc 033e00adafc5d7bc9402013f261b9f7de65938d4ce9f66f2ea4ec1419a0ae286 # shrinks to value = "A@0@a"
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
[workspace]
|
||||
dependencies_update = true
|
||||
git_release_enable = false
|
||||
semver_check = true
|
||||
pr_branch_prefix = "release-plz/"
|
||||
pr_name = "chore(release): prepare for publishing"
|
||||
changelog_config = "cliff.toml"
|
||||
release_commits = "^(feat|docs|fix|build|perf)"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user