Compare commits

...

56 Commits

Author SHA1 Message Date
Steffen Eiden
03f7566436 New release s390-tools-2.44.0
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-31 15:51:50 +02:00
Marc Hartmayer
177d8ae0dd pvimg: Get rid of most 'ignore' doctest directives
Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-31 09:51:14 +02:00
Marc Hartmayer
08d21b24b4 pvimg: Use indexed array for UV key hashes
Replace individual UV key hash fields with an indexed array and
introduce dedicated types for hash indices and key hash kinds. This
simplifies hash handling.

Co-developed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-31 09:51:14 +02:00
Marc Hartmayer
ed74e84862 pvimg: Report host-key document type mismatches
Report host-key document type mismatches before hash verification.

For example, when testing a v2 Secure Execution header with a classical
host-key document, report that a hybrid host-key document is required
instead of failing with a generic header validation error.

Before:

$ pvimg -- test --host-key-document v1.hkd se.v2.img
error: Invalid Secure Execution header

After:

$ pvimg -- test --host-key-document v1.hkd se.v2.img
Host-key document 'v1.hkd' is not a hybrid host-key document.
error: The provided host key document in v1.hkd contains the wrong number of keys!

Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-31 09:51:14 +02:00
Marc Hartmayer
0263637d9f pv: Refactor 'get_verified_hkds'
Get rid of 'read_hkd' by refactoring the 'get_verified_hkds' function.
For this a new HkdLoader::load_and_verify is introduced that is a
reworked version of the original code.

In addition, add test cases for testing all the edge cases.

Assisted-by: IBM Bob:1.0.6
Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-31 09:51:14 +02:00
Mete Durlu
1aa1558f91 zmemtopo: Add case-insensitive partition name filtering
Introduce the -p/--partition-filter option to display only partitions
whose names contain a specified substring. The filter performs
case-insensitive matching and applies consistently across all output
formats (table, tree, and reverse tree views).

Example:

$ zmemtopo -p "part74"
LPAR/LEVEL      SIZE
PARTITION74      12G
└LEVEL4_1        12G
  ├LEVEL3_0       3G
  ├LEVEL3_1       3G
  ├LEVEL3_2       3G
  └LEVEL3_3       3G

Suggested-by: Niklas Schnelle <schnelle@linux.ibm.com>
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Mete Durlu <meted@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-31 09:51:14 +02:00
Mete Durlu
29a1ef0023 zmemtopo: Fix table view spacing
Left justify partition number and partition name fields on table view
to better accommodate untrimmed and longer partition names.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Mete Durlu <meted@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-31 09:51:14 +02:00
Mete Durlu
39eb46763b zmemtopo: Remove false leading/trailing space trimming
Remove all util_strstrip() calls. The current users are command line
option stripping and partition name stripping.
Keep user passed arguments as is, typical shells already remove leading
and trailing whitespace unless explicitly quoted.
Spaces in partition names are valid. Keep the leading/trailing spaces
to preserve user given partition names.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Mete Durlu <meted@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-31 09:51:14 +02:00
Mete Durlu
ce1464ad5f zmemtopo: Clean up hardcoded value
Replace array length of lpar name with LPAR_NAME_LEN macro.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Mete Durlu <meted@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-31 09:51:14 +02:00
Steffen Eiden
c6d74eebfb pvimg: Fix hash comparison in SeHdrAadV2::contains_hash
Ensure both sides of the hash comparison use the same slice length
by explicitly slicing both the keyslot hash and the input hash to
UV_KEY_HASH_SIZE. Previously, only the keyslot hash was sliced while
comparing against the full input hash reference, which could lead to
incorrect comparisons if the input hash length differs.

This makes the comparison more explicit and ensures we're always
comparing equal-length hashes. This is useful if one hash is a subset of
another, e.g. a sha512 hash truncated to 32 bytes.

Fixes: 89577c2f8c ("pvimg: Use hybrid keys")
Reviewed-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-31 09:51:14 +02:00
Marc Hartmayer
ad129057b5 pvattest: tests: Implement TODO
Instead of blindly casting the values to u32 use try_into().unwrap().

Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:01 +02:00
Marc Hartmayer
5b8fdc2bec pv: Improve API by marking some functions as must_use
Make the API less prone to error by specifying some of them as must_use.
This avoids issues that the result is not used.

Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:01 +02:00
Marc Hartmayer
784cff18c3 pv: Reuse RequestVersion and RequestMagic
Use the typedefs to make the intention and compatibility of the fields
more clear.

Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:01 +02:00
Timo Keller
fb33935c9b pvsecret: test using hybrid keys
Add unite tests and (binary) test asses for the hybrid key patch.

Co-developed-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Timo Keller <tkeller@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:01 +02:00
Marc Hartmayer
175b336d32 rust/utils: Replace get_verified_hkds with get_verified_hkds_new
Now as no users of get_verified_hkds exists, replace it with
get_verified_hkds_new.

Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:01 +02:00
Steffen Eiden
9a5c9cd7f9 pv: Require matching versions of request and HKD
Enforce that a v{1,2} request also has a v{1,2} hostkey.
This requires to change the signature of Request::add_hostkey to return
a Result.

Co-Developed-by: Timo Keller <tkeller@linux.ibm.com>
Signed-off-by: Timo Keller <tkeller@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
Reviewed-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:01 +02:00
Steffen Eiden
73c227fa9a pvattest: Verify hybrid keys
Allow pvattest verify to verify hybrid keys. The hybrid key is
represented by a sha512 hash truncated to 32 bytes.

Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
Reviewed-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:01 +02:00
Steffen Eiden
1078e6d45f pvverify: Use hybrid keys
Allow pvverify to also verify hybrid keys. By default the key format is
autodetected.

Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
Reviewed-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:01 +02:00
Timo Keller
50808edb7c pvsecret: Use hybrid keys
Allow the creation of Add-secret requests using hybrid (=quantum safe)
keys. This results in using the headers in version 2 (0x200).

Co-developed-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Timo Keller <tkeller@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:01 +02:00
Timo Keller
9dca2d3181 pvattest: Use hybrid keys
Allow the creation of Attestation requests using hybrid (=quantum safe)
keys. This results in using the headers in version 2 (0x200).

Co-developed-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Timo Keller <tkeller@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:00 +02:00
Timo Keller
fc853f3259 pvimg/create-sehdr: Use hybrid keys
Allow the creation of SE headers with hybrid (=quantum safe) keys. This
results in using the headers in version 2 (0x200).

Co-developed-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Timo Keller <tkeller@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:00 +02:00
Timo Keller
89577c2f8c pvimg: Use hybrid keys
Allow the creation of SE images using headers with hybrid (=quantum safe) keys.
This results in using the headers in version 2 (0x200).

Co-developed-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Timo Keller <tkeller@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:00 +02:00
Marc Hartmayer
d6fc4921fb utils: Read hybrid HKDs
* Add `HkdVersion` (classic or hybrid) and `HkdVersionSelection`.
* Add `get_verified_hkds_new` that returns a list of verified HKDs
  read according to the given `HkdVersionSelection`.

Co-Developed-by: Timo Keller <tkeller@linux.ibm.com>
Signed-off-by: Timo Keller <tkeller@linux.ibm.com>
Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:00 +02:00
Marc Hartmayer
cb9c2f993c utils/cli: Introduce AutoOrExplicit
* Add `AutoOrExplicit` that dispatches between `Auto` and `Explicit`.
* Add `AutoOrExplicitParser` for it.

In a later commit, this will be used for auto detection of HKD version
or explicitly specifying the version.

Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:00 +02:00
Marc Hartmayer
637c8196a6 pvimg: tests: Refactor test code and increase coverage
Refactor test code and increase coverage. The new test code tests for
the correct error kind instead of simply for an error.

Assisted-by: IBM Bob:1.0.6
Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:00 +02:00
Marc Hartmayer
c87d11abfe pvimg and create-sehdr: Refactor code to use generic flags
Replace version-specific flag enums (PcfV1, ScfV1) with a unified
SeHdrFlag enum and generic ControlFlagsModel. This refactoring provides:

 - Single SeHdrFlag enum for all control flags across SE header versions
 - Generic SeHdrControlFlagsModel with version-specific configurations
 - EffectiveControlFlags type replacing PlaintextControlFlagsV1/SecretControlFlagsV1
 - FlagsOverride mechanism for customizing flag configurations
 - Enhanced FromStr implementation for flag parsing
 - Comprehensive test coverage for flag operations and conversions

The new design improves maintainability by eliminating code duplication
and provides a more flexible API for working with SE header control
flags.

Assisted-by: IBM Bob:1.0.4
Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:00 +02:00
Marc Hartmayer
20fffb31d7 pvimg: Introduce generic flags
Add a generic flags framework for managing (Secure Execution) header
control flags. This implementation provides:

  - Clap compatibility for auto-completion and proper help messages
  - ControlFlagTrait for defining flag types with bit positions and version support
  - ControlFlagsModel for managing default flags and validation rules
  - FlagsOverride for customizing individual flag states
  - EffectiveControlFlags representing the final flag configuration
  - UnknownFlags handling for forward compatibility
  - Comprehensive error handling via FlagValidationError
  - Display implementations for both human-readable and binary formats
  - Type-safe conversions between flag representations

The framework supports version-aware flag validation, allowing flags to
be marked as supported only in specific Secure Execution header
versions. It enables flexible flag configuration through overrides while
maintaining type safety and validation.

Assisted-by: IBM Bob:1.0.4
Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:00 +02:00
Marc Hartmayer
2d330cd45f utils_macros: Implement derive_control_flag
Add a new derive macro 'derive_control_flag' that is used in the next
commit to reimplement how the code deals with Secure Execution control
flags.

It implements Display, IntoEnumIterator and the ControlFlagTrait for
enums using unit variants only.

  /// Trait for control flags that provide bit position information.
  pub trait ControlFlagTrait {
      /// Returns the bit position for this flag.
      fn bit_position(self) -> u8;
  }

Assisted-by: IBM Bob:1.0.4
Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:00 +02:00
Marc Hartmayer
618e22e38b utils: Add utils_macros: Implement ValueEnumDisplay and ValueEnumFromStr macros
Add new derive macros ValueEnumDisplay and ValueEnumFromStr which can be
used to derive 'Display' and 'FromStr' for enums implementing
'clap::ValueEnum'. It helps in reducing boilerplate code and keep things
in sync.

Assisted-by: IBM Bob:1.0.4
Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:00 +02:00
Marc Hartmayer
173a055372 pvimg: Use log level info for printing used flags
Use log level info for printed the used flags, instead of log level
debug.

Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:00 +02:00
Timo Keller
4670b108a6 pv: Implement HostKey::V2 and Keyslot::V2
Introduce HybridPublicKey combining ECDH and ML-KEM public keys. Use it
for HostKey::V2 and add Keyslot::V2 for hybrid keyslots.

Add tests for the new functionality.

Assisted-by: IBM Bob:1.0.5
Signed-off-by: Timo Keller <tkeller@linux.ibm.com>
Reviewed-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:00 +02:00
Timo Keller
ebe56592c8 pv: Add hybrid key derivation support
Add derive_aes256_gcm_key_hybrid() function that derives an Aes256GcmKey
and ML-KEM1024 ciphertext from a private ECDH customer key, a public
ECDH target key, and a public ML-KEM target key. This enables hybrid
post-quantum cryptography support.

Assisted-by: IBM Bob:1.0.5
Co-developed-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Timo Keller <tkeller@linux.ibm.com>
Acked-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:00 +02:00
Marc Hartmayer
1746d2bb6a pv: Change ec_key() to return Option<&PKeyRef<Public>>
Prepare the API for future host keys that might not have an EC key by
returning Option instead of a direct reference. Additionally, use
&PKeyRef<Public> as the borrowed view on PKey for better API
consistency.

Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Timo Keller <tkeller@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
2026-07-28 11:00:00 +02:00
Niklas Schnelle
91123e5d45 lib/zt_common.h: zpcimon: Add __force and use it to annotate le32toh() conversions
When using sparse on zpcimon several warnings like the one below are
generated:

  warning: incorrect type in argument 1 (different base types)
     expected unsigned int [usertype] __bsx
     got restricted __le32 [usertype] warning_temp_time

This is because several members in struct nvme_smart_log are marked as
__le32. These members are correctly converted to host endianness before
use via le32toh() respectively le16toh(). Since these functions take
their parameters as plain uint32_t or uint16_t however the implicit
conversion triggers the above warning. Fix this by adding the __force
attribute and using it to mark type conversions in the leXXtoh() calls.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:51 +02:00
Niklas Schnelle
0f0e8cf33b zpcimon: Make optics_los_implemente() static
Sparse noted that optics_los_implemente() can be static. Make it so.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:51 +02:00
Niklas Schnelle
7586a1ad59 zpcimon: Add missing include for __packed attribute
The __packed attribute is used without including the necessary
zt_common.h header, add it. Note that luckily the struct as it currently
exists doesn't actually required __packed as the natural alignment
leaves no holes. Still if in the future the reserved field is split up
it would need it and this keeps it consistent with the kernel
declaration.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:51 +02:00
Niklas Schnelle
1da328921b dbginfo.sh: Let zpcimon log both optical module and SMART data
Add zpcimon data collection with both SMART log and optical module data.
Do so in the Z device subsystem commands section to account for the fact
that zpcimon is no longer network specific. At the same time keep the
existing opticsmon invocation for compatibility when executed on systems
without zpcimon.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Reviewed-by: Jörn Siglen <siglen@de.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:51 +02:00
Niklas Schnelle
f96f09a5c9 nvmemon: libzpci: Skip SCLP on NVMes with non-IBM subsystem vendor ID
The SCLP Write Event Data Action Qualifier 4 is only available for
NVMes with subsystem vendor ID matching IBM. Add both IDs to struct
zpci_dev in libzpci. Also add them to the libzpci_example output
and adjust its output to multiline to handle more data.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:51 +02:00
Niklas Schnelle
4fd607ae5c zpcimon: Monitor for hotplug of NVMes to trigger SMART collection
Use libudev to monitor for "change" events in the "nvme" subsystem that
use the "pcie" transport to trigger NVMe SMART data collection. Only
collect data from the NVMe referenced by the udev event.

Assisted-by: IBM Bob:1.0.1
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:51 +02:00
Niklas Schnelle
f1a11a01df zpcimon: Add JSON output for SMART data details
This uses the same format as:

   nvme smart-log --output-format=json /dev/nvmeX

One complication being that several values in the SMART data use
unsigned 128 bit integers. Both GCC and Clang support __uint128_t as
a C extension but don't offer printf() support for it. Just like
nvme-cli add a custom uint128_t to string function..

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:50 +02:00
Niklas Schnelle
2eceedeb11 zpcimon: Add NVMe SMART data monitor
Add a new monitor which uses libnvme to collect SMART data from NVMes.
This monitor only implements the .collect_adapter_data operation since
it carries no state across data collections. Nevertheless for future
symmetry and possible future expansion also add an empty struct
nvmemon_ctx.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:50 +02:00
Niklas Schnelle
a9dbb8a1a2 zpcimon: Factor out JSON printing of common PCI adapter attributes
Some of the attributes printed as JSON by opticsmon are common to all
PCI adapters factor these out into reusable helper functions
zpci_adapter_json_print_start() and zpci_adapter_json_print_end().

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:50 +02:00
Niklas Schnelle
78dd82d129 libzpci: Extract get_device_node() from zpcictl to libzpci
The get_device_node() function for getting the name of an NVMe's
controller device e.g. "nvme0" as implemented in zpcictl is generally
useful and will be needed for NVMe SMART data collection in zpcimon as
well. Move it to libzpci and rename it to zpci_get_nvme_device_node() to
account for the change in namespace.

It would be tempting to pass a struct zpci_dev* instead of a const char*
but as zpcictl uses an incompatible struct zpci_device* this will
require a larger rework. Prioritize minimizing code duplication for now.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:50 +02:00
Niklas Schnelle
113e9ebfef zpcimon: Split optics monitoring out into separate monitor
Introduce the concept of a generic zPCI device monitor by creating the
zpcimon_ops operations struct turning all optics specific calls into
abstract monitor calls. Handle monitors as a static array of zpcimon_ops
based monitors of which the now split out optics monitor is currently
the only one. All operations are in principle optional though a monitor
which provides an init operation must also provide the corresponding
destroy operation.

Keep the base64 based JSON pair as non optics specific for later re-use
and make it just skip the output in the very unlikely case that encoding
fails. In follow on commits a monitor for NVMe devices collecting SMART
data will be added.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:50 +02:00
Niklas Schnelle
f9ef8a0106 util_time: zpcimon: Use a constant for seconds per day
Replace the magic value 86400 with a new util_time.h constant
SEC_PER_DAY.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:50 +02:00
Niklas Schnelle
2c63c69af4 zpcimon: Allow setting output format and use JSON Lines in systemd unit
Introduce separate '--format' option to set output format. Use the new
JSON Lines format explicitly in the systemd unit. Since JSON Lines
format only uses printable characters this removes the need for passing
'-a' to systemctl status. At the same time JSON-SEQ is more easily
parsable using 'jq --seq' so keep it the default for easier scripting.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:50 +02:00
Niklas Schnelle
8a4a4e0557 libutil/util_fmt: Add util_fmt_type_to_name()
Add a function to get the textual name of an enum util_fmt_t value. To
make this robust to changes in the order of elements in the format array
initialize this using named indices.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:50 +02:00
Niklas Schnelle
e8550a4f8d opticsmon: zpcimon: Rename opticsmon to zpcimon
The opticsmon tool started out as a tool for monitoring the health of
optical modules in directly attached PCI NICs. In the future however it
will also monitor the health of other PCI devices. In particular in
a first step it will monitor the health of directly attached NVMe
devices.

To reflect this broadening of its scope rename opticsmon to zpcimon. Add
zpcimon.service and install it both under the new name and symlinked as
opticsmon.service for backwards compatibility. Since users are expected
to mostly just enable the service this keeps old instructions just
working.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:50 +02:00
Niklas Schnelle
e7bc4a5d31 opticsmon: Enforce 1 second as minimal interval
So far it was possible for the user to specify an interval of 0 which
would result in not re-arming the timer at all thus disabling interval
based data collection. While this would still do optics data collection
on startup and link changes the intention is to collect data at least
every 24 hours. If interval is given as 0 treat this as minimum update
interval of 1 second.

Fixes: c34adb9cab ("opticsmon: Introduce opticsmon tool")
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:50 +02:00
Niklas Schnelle
8b38bf37b0 opticsmon: Fix wrong size check for OPTICS_QSFP28_LOS_IMPLEMENTED_OFFSET
The code accesses OPTICS_QSFP28_LOS_IMPLEMENTED_OFFSET so the size needs
to be one larger for the LOS to be accessible.

Fixes: c34adb9cab ("opticsmon: Introduce opticsmon tool")
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:50 +02:00
Niklas Schnelle
3696d6bd08 opticsmon: Handle error return of ethtool_nl_connect()
Don't just ignore the return of ethtool_nl_connect(). This would also
otherwise lead to invalid socket accesses later. Also make
ethtool_nl_connect() return negative error values instead of
EXIT_FAILURE.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:50 +02:00
Niklas Schnelle
6847b6a8cf opticsmon: Fix error path free of struct optics in ethtool_nl_get_optics()
When ethtool_nl_get_optics() fails after ethtool_nl_cb() has allocated
oi->raw this memory would leak because only the struct optics* itself
was freed. Fix this by using optics_free() and relying on free(oi->raw)
being a no-op for NULL pointers in combination with using util_zalloc()
to handle errors both before and after oi->raw was allocated.

Fixes: c34adb9cab ("opticsmon: Introduce opticsmon tool")
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:50 +02:00
Niklas Schnelle
1f339a9e7e opticsmon: Return errors from monitor_wait_loop() in monitor_mode()
Don't ignore the return value of monitor_wait_loop() but instead forward
it to the caller.

Fixes: c34adb9cab ("opticsmon: Introduce opticsmon tool")
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:50 +02:00
Niklas Schnelle
905c3921af opticsmon: Close epoll fd in monitor_wait_loop()
The epoll fd (epfd) in monitor_wait_loop() is never closed. Close it in
all return paths. Also there is no explicit check for an error return of
epoll_create1() add that too.

Fixes: c34adb9cab ("opticsmon: Introduce opticsmon tool")
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:50 +02:00
Niklas Schnelle
e55e0e470a opticsmon: Fix wrong JSON print for tx_fault
The "tx_fault" field in the JSON output accidentally contained the
rx_los value instead of the tx_fault one. Change it to the correct
value. Note that this does not impact the data relayed to firmware as
that uses the raw module data.

Fixes: c34adb9cab ("opticsmon: Introduce opticsmon tool")
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:50 +02:00
Jan Höppner
9d9fca6e72 Prepare for next release
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-22 18:49:20 +02:00
119 changed files with 9764 additions and 1609 deletions

8
.gitignore vendored
View File

@@ -102,9 +102,11 @@ lsstp/_lsstp
lsstp/lsstp.bash
mon_tools/mon_fsstatd
mon_tools/mon_procd
opticsmon/opticsmon
opticsmon/_opticsmon
opticsmon/opticsmon.bash
zpcimon/zpcimon
zpcimon/zpcimon.bash
zpcimon/_zpcimon
zpcimon/opticsmon.bash
zpcimon/_opticsmon
osasnmpd/osasnmpd
qetharp/qetharp
qethqoat/qethqoat

View File

@@ -1,6 +1,32 @@
Release history for s390-tools (MIT version)
--------------------------------------------
* __v2.44.0 (2026-07-31)__
For Linux kernel version: 7.2
Changes of existing tools / libraries:
- create-sehdr: Enable quantum safe keys usage
- dbginfo.sh: Let zpcimon log both optical module and SMART data
- libutil/util_fmt: Add util_fmt_type_to_name()
- nvmemon: Skip SCLP on NVMes with non-IBM subsystem vendor ID
- opticsmon: zpcimon: Rename opticsmon to zpcimon
- pvattest: Enable quantum safe keys usage
- pvimg: Add '--flags <...>' and '--disable-flags <...>' option
- pvimg: Enable quantum safe keys usage
- pvsecret: Enable quantum safe keys usage
- pvverify: Enable quantum safe keys usage
- zmemtopo: Add CLI option to filter partitions by name
- zpcimon: Allow setting output format using --format CLI option
- zpcimon: Monitor for hotplug of NVMes to trigger SMART collection
Bug Fixes:
- opticsmon: Fix wrong size check for OPTICS_QSFP28_LOS_IMPLEMENTED_OFFSET
- opticsmon: Handle error return of ethtool_nl_connect()
- opticsmon: Fix error path free of struct optics in ethtool_nl_get_optics()
- opticsmon: Close epoll fd in monitor_wait_loop()
- opticsmon: Fix wrong JSON print for tx_fault
* __v2.43.1 (2026-07-22)__
For Linux kernel version: 7.1

View File

@@ -15,7 +15,7 @@ TOOL_DIRS = zipl zdump fdasd dasdfmt dasdview tunedasd \
vmcp man mon_tools dasdinfo vmur cpuplugd ipl_tools \
ziomon iucvterm hyptop cmsfs-fuse qethqoat zfcpdump zdsfs cpumf \
systemd hmcdrvfs cpacfstats zdev dump2tar zkey netboot etc zpcictl \
lsstp hsci hsavmcore chreipl-fcp-mpath ap_tools rust opticsmon \
lsstp hsci hsavmcore chreipl-fcp-mpath ap_tools rust zpcimon \
zpwr zmemtopo
else

View File

@@ -316,7 +316,7 @@ build options:
| glibc-static | `HAVE_LIBC_STATIC` | zfcpdump |
| openssl | `HAVE_OPENSSL` | zkey, libekmfweb, libkmipclient, |
| | | zgetdump, rust/pvattest, rust/pvimg, |
| | | zgetdump/pvsecret, opticsmon |
| | | zgetdump/pvsecret, zpcimon |
| cryptsetup | `HAVE_CRYPTSETUP2` | zkey-cryptsetup |
| json-c | `HAVE_JSONC` | zkey-cryptsetup, libekmfweb, |
| | | libkmipclient |
@@ -325,8 +325,9 @@ build options:
| | | rust/pvattest, rust/pvsecret, |
| libxml2 | `HAVE_LIBXML2` | libkmipclient |
| systemd | `HAVE_SYSTEMD` | hsavmcore |
| libudev | `HAVE_LIBUDEV` | cpacfstatsd |
| libnl3 | `HAVE_LIBNL3` | opticsmon |
| libudev | `HAVE_LIBUDEV` | cpacfstatsd, zpcimon |
| libnl3 | `HAVE_LIBNL3` | zpcimon |
| libnvme | `HAVE_LIBNVME` | zpcimon |
This table lists additional build or install options:
@@ -374,11 +375,11 @@ the different tools are provided:
The runtime requirements are: openssl-libs (>= 1.1.1) and libcurl.
* opticsmon:
For building opticsmon OpenSSL and the Netlink Library Suite (libnl3) are
required.
Tip: you may skip the opticsmon build by adding
`HAVE_OPENSSL=0` or `HAVE_LIBNL3=0`
* zpcimon:
For building zpcimon OpenSSL, the Netlink Library Suite (libnl3),
and libnvme are required.
Tip: you may skip the zpcimon build by adding
`HAVE_OPENSSL=0` or `HAVE_LIBNL3=0` or `HAVE_LIBNVME=0`.
* osasnmpd:
You need at least the NET-SNMP 5.1.x package (net-snmp-devel.rpm)

View File

@@ -32,8 +32,8 @@ endif
# The variable "DISTRELEASE" should be overwritten in rpm spec files with:
# "make DISTRELEASE=%{release}" and "make install DISTRELEASE=%{release}"
VERSION := 2
RELEASE := 43
PATCHLEVEL := 1
RELEASE := 44
PATCHLEVEL := 0
DISTRELEASE := build-$(shell date +%Y%m%d)
S390_TOOLS_RELEASE := $(VERSION).$(RELEASE).$(PATCHLEVEL)-$(DISTRELEASE)
export S390_TOOLS_RELEASE

View File

@@ -17,6 +17,9 @@
#include "util_list.h"
#define PCI_BDF_LEN 13 /* DDDD:BB:dd.f\0 */
#define PCI_VENDOR_ID_IBM 0x1014
enum zpci_pft {
ZPCI_PFT_UNCLASSIFIED = 0x00,
@@ -65,6 +68,9 @@ struct zpci_dev {
bool uid_is_unique;
/* Configuration state 0 - Standby, 1 Configured */
bool conf;
/* Common PCI attributes */
uint16_t subsystem_vendor;
uint16_t subsystem_device;
/* Associated netdevs if any */
int num_netdevs;
@@ -96,4 +102,6 @@ operstate_t zpci_operstate_from_str(const char *oper_str);
struct zpci_dev *zpci_find_by_netdev(struct util_list *zpci_list, char *netdev_name,
struct zpci_netdev **netdev);
char *zpci_get_nvme_device_node(const char *pci_addr);
#endif /* LIB_ZPCI_PCI_LIST_H */

View File

@@ -21,9 +21,11 @@
#define SCLP_ERRNOTIFY_AQ_DECONF 1
#define SCLP_ERRNOTIFY_AQ_REPORT_ERR 2
#define SCLP_ERRNOTIFY_AQ_OPTICS_DATA 3
#define SCLP_ERRNOTIFY_AQ_NVME_SMART_DATA 4
#define SCLP_ERRNOTIFY_ID_ZPCICTL 0x4713
#define SCLP_ERRNOTIFY_ID_OPTICSMON 0x4714
#define SCLP_ERRNOTIFY_ID_NVMEMON 0x4715
#define SCLP_ERRNOTIFY_DATA_SIZE 4054

View File

@@ -157,6 +157,16 @@ void util_fmt_exit(void);
*/
bool util_fmt_name_to_type(const char *name, enum util_fmt_t *type);
/**
* util_fmt_type_to_name() - Get name of given format type identifier.
* @type: Format type identifier
*
* Get the name corresponding to the given format type identifier.
*
* Return: name of the format type
*/
const char *util_fmt_type_to_name(enum util_fmt_t type);
/**
* util_fmt_set_indent() - Set indentation parameters.
* @base : Base indentation level to apply to all output lines (default 0)

View File

@@ -19,6 +19,8 @@
#define MSEC_PER_SEC 1000L
#define SEC_PER_DAY (24 * 60 * 60)
/**
* Convert nanoseconds to microseconds
*

View File

@@ -72,6 +72,11 @@
#define __section(x) __attribute__((__section__(#x)))
#define __noinline __attribute__((__noinline__))
#define __big_endian
#ifdef __CHECKER__
# define __force __attribute__((force))
#else
# define __force
#endif
/* The Linux kernel (in stddef.h) and glibc (sys/cdefs.h) define
* __always_inline. Therefore undefine it first to allow the headers
* to be included first.

View File

@@ -89,11 +89,26 @@ static const struct {
const char *name;
enum util_fmt_t fmt;
} formats[] = {
{ "json", FMT_JSON },
{ "json-seq", FMT_JSONSEQ },
{ "jsonl", FMT_JSONL },
{ "pairs", FMT_PAIRS },
{ "csv", FMT_CSV },
[FMT_JSON] = {
.name = "json",
.fmt = FMT_JSON
},
[FMT_JSONSEQ] = {
.name = "json-seq",
.fmt = FMT_JSONSEQ
},
[FMT_JSONL] = {
.name = "jsonl",
.fmt = FMT_JSONL
},
[FMT_PAIRS] = {
.name = "pairs",
.fmt = FMT_PAIRS
},
[FMT_CSV] = {
.name = "csv",
.fmt = FMT_CSV
},
};
/* Signal mask for blocking INT and TERM signals. */
@@ -112,6 +127,11 @@ bool util_fmt_name_to_type(const char *name, enum util_fmt_t *type)
return false;
}
const char *util_fmt_type_to_name(enum util_fmt_t type)
{
return formats[type].name;
}
bool util_fmt_is_json(enum util_fmt_t type)
{
switch (type) {

View File

@@ -12,13 +12,17 @@ static void zpci_print(struct zpci_dev *zdev)
int i;
if (!zdev->conf) {
printf("fid: %8x address: %s\n", zdev->fid, pci_addr);
printf("fid:\t%4x address: %s\n", zdev->fid, pci_addr);
} else {
printf("fid: %8x address: %s uid: %4x%s pchid: %4x vfn: %4d port: %1d pft: %s ",
printf("fid:\t%4x address: %s uid: %4x%s pchid: %4x vfn: %4d\n",
zdev->fid, pci_addr, zdev->uid, (zdev->uid_is_unique) ? " (unique)" : "",
zdev->pchid, zdev->vfn, zdev->port, zpci_pft_str(zdev));
zdev->pchid, zdev->vfn);
printf("\n");
printf("\tport: %1d pft: %s subsystem_device: %4x subsystem_vendor: %4x\n",
zdev->port, zpci_pft_str(zdev), zdev->subsystem_device,
zdev->subsystem_vendor);
if (zdev->num_netdevs) {
printf("netdevs: ");
printf("\tnetdevs: ");
for (i = 0; i < zdev->num_netdevs; i++) {
printf("%s (%s)", zdev->netdevs[i].name,
zpci_operstate_str(zdev->netdevs[i].operstate));

View File

@@ -20,6 +20,7 @@
#include "lib/util_list.h"
#include "lib/util_path.h"
#include "lib/util_scandir.h"
#include "lib/util_sys.h"
/**
* Get the function type name for the given device
@@ -137,7 +138,7 @@ const char *zpci_operstate_str(operstate_t state)
static int zpci_populate_from_slot_dir(struct zpci_dev *zdev, const char *slot_dir,
const char *slot_name)
{
char buf_addr[11]; /* "dddd:bb:dd\0" */
char buf_addr[PCI_BDF_LEN];
uint8_t bus, df;
uint32_t domain;
int val, rc;
@@ -261,6 +262,16 @@ static int zpci_populate_from_dev_dir(struct zpci_dev *zdev)
goto out_path;
zdev->pft = val;
rc = util_file_read_i(&val, 16, "%s/subsystem_device", path);
if (rc)
goto out_path;
zdev->subsystem_device = val;
rc = util_file_read_i(&val, 16, "%s/subsystem_vendor", path);
if (rc)
goto out_path;
zdev->subsystem_vendor = val;
if (util_path_is_readable("%s/net", path)) {
rc = zpci_populate_netdevices(zdev, path);
if (rc)
@@ -387,3 +398,46 @@ struct zpci_dev *zpci_find_by_netdev(struct util_list *zpci_list, char *netdev_n
}
return NULL;
}
/**
* Get the NVMe device file name given a PCI address
*
* This function retrieves the NVMe device file "/dev/nvmeX"
* for a given PCI address. The device name can be used to construct
* the path /dev/nvmeX which is the NVMe's controller's character
* device used for example to retrieve S.M.A.R.T. data.
*
* @param[in] pci_addr The "DDDD:bb:dd.f" format PCI address
*
* @return The NVMe device file name if one is found NULL otherwise
*/
char *zpci_get_nvme_device_node(const char *pci_addr)
{
char *path, *dev = NULL;
char dev_addr[PCI_BDF_LEN];
struct dirent **de_vec;
int count, i;
path = util_path_sysfs("bus/pci/devices/%s/nvme", pci_addr);
count = util_scandir(&de_vec, alphasort, path, "nvme*");
if (count == -1) {
warnx("Could not read directory %s: %s", path, strerror(errno));
goto exit_path;
}
for (i = 0; i < count; i++) {
util_asprintf(&dev, "/dev/%s", de_vec[i]->d_name);
if (util_sys_get_dev_addr(dev, dev_addr) != 0)
goto free_continue;
if (strcmp(dev_addr, pci_addr) == 0)
break;
free_continue:
free(dev);
dev = NULL;
}
util_scandir_free(de_vec, count);
exit_path:
free(path);
return dev;
}

View File

@@ -1,407 +0,0 @@
/*
* opticsmon - Report optics monitoring data to firmware
*
* Copyright IBM Corp. 2024
*
* s390-tools is free software; you can redistribute it and/or modify
* it under the terms of the MIT license. See LICENSE for details.
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>
#include <sys/epoll.h>
#include <sys/signalfd.h>
#include <signal.h>
#include <sys/timerfd.h>
#include <time.h>
#include <linux/if.h>
#include "lib/util_list.h"
#include "lib/pci_list.h"
#include "lib/util_prg.h"
#include "lib/util_opt.h"
#include "lib/util_fmt.h"
#include "lib/util_libc.h"
#include <openssl/evp.h>
#include "ethtool.h"
#include "link_mon.h"
#include "optics_info.h"
#include "optics_sclp.h"
#include "opticsmon_cli.h"
#define API_LEVEL 1
struct options {
bool monitor;
bool report;
bool module_info;
bool quiet;
uint32_t interval_seconds;
};
struct opticsmon_ctx {
struct options opts;
struct ethtool_nl_ctx ethtool_ctx;
struct link_mon_nl_ctx lctx;
struct util_list *zpci_list;
};
static const struct util_prg prg = {
.desc = "Use opticsmon to monitor the health of the optical modules\n"
"of directly attached PCI based NICs",
.copyright_vec = { {
.owner = "IBM Corp.",
.pub_first = 2024,
.pub_last = 2024,
},
UTIL_PRG_COPYRIGHT_END }
};
static void parse_cmdline(int argc, char *argv[], struct options *opts)
{
uint32_t seconds;
int cmd, ret;
util_prg_init(&prg);
util_opt_init(opt_vec, NULL);
do {
cmd = util_opt_getopt_long(argc, argv);
switch (cmd) {
case 'm':
opts->monitor = true;
break;
case 'r':
opts->report = true;
break;
case 'q':
opts->quiet = true;
break;
case OPT_DUMP:
opts->module_info = true;
break;
case 'i':
ret = sscanf(optarg, "%u", &seconds);
if (ret != 1) {
fprintf(stderr,
"Failed to parse interval argument \"%s\" as seconds\n",
optarg);
exit(EXIT_FAILURE);
}
if (seconds < 86400)
opts->interval_seconds = seconds;
break;
case 'h':
util_prg_print_help();
util_opt_print_help();
exit(EXIT_SUCCESS);
case 'v':
util_prg_print_version();
exit(EXIT_SUCCESS);
case -1:
/* End of options string */
break;
}
} while (cmd != -1);
}
static int module_info_pair(struct optics *oi)
{
size_t b64_calclen, b64len;
int rc = EXIT_SUCCESS;
char *b64;
b64_calclen = (oi->size / 3) * 4;
if (oi->size % 3 > 0)
b64_calclen += 4;
b64 = util_zalloc(b64_calclen + 1); /* adds NUL byte */
b64len = EVP_EncodeBlock((unsigned char *)b64, oi->raw, oi->size);
if (b64len != b64_calclen) {
fprintf(stderr, "encoding base64 via openssl failed\n");
rc = EXIT_FAILURE;
goto out;
}
util_fmt_pair(FMT_QUOTE, "module_info", b64);
out:
free(b64);
return rc;
}
static void optics_json_print(struct opticsmon_ctx *ctx, struct zpci_netdev *nd, struct optics *oi)
{
util_fmt_obj_start(FMT_DEFAULT, "netdev");
util_fmt_pair(FMT_QUOTE, "name", nd->name);
util_fmt_pair(FMT_QUOTE, "operstate", zpci_operstate_str(nd->operstate));
util_fmt_obj_start(FMT_DEFAULT, "optics");
util_fmt_pair(FMT_QUOTE, "type", optics_type_str(optics_type(oi)));
util_fmt_pair(FMT_QUOTE, "rx_los", optics_los_str(optics_rx_los(oi)));
util_fmt_pair(FMT_QUOTE, "tx_los", optics_los_str(optics_tx_los(oi)));
util_fmt_pair(FMT_QUOTE, "tx_fault", optics_los_str(optics_rx_los(oi)));
if (ctx->opts.module_info)
module_info_pair(oi);
util_fmt_obj_end();
util_fmt_obj_end();
}
static int dump_adapter_data(struct opticsmon_ctx *ctx, struct zpci_dev *zdev)
{
struct optics **ois;
int num_ois = 0;
char *pci_addr;
int i, rc;
ois = util_zalloc(sizeof(ois[0]) * zdev->num_netdevs);
for (i = 0; i < zdev->num_netdevs; i++) {
rc = ethtool_nl_get_optics(&ctx->ethtool_ctx, zdev->netdevs[i].name, &ois[i]);
if (rc)
goto free_ois;
num_ois++;
}
if (!ctx->opts.quiet) {
util_fmt_obj_start(FMT_DEFAULT, "adapter");
util_fmt_pair(FMT_QUOTE, "pft", zpci_pft_str(zdev));
util_fmt_obj_start(FMT_DEFAULT, "ids");
util_fmt_pair(FMT_QUOTE, "fid", "0x%0x", zdev->fid);
if (zdev->uid_is_unique)
util_fmt_pair(FMT_QUOTE, "uid", "0x%0x", zdev->uid);
pci_addr = zpci_pci_addr(zdev);
util_fmt_pair(FMT_QUOTE, "pci_address", pci_addr);
free(pci_addr);
util_fmt_obj_end();
util_fmt_obj_start(FMT_LIST, "netdevs");
for (i = 0; i < zdev->num_netdevs; i++)
optics_json_print(ctx, &zdev->netdevs[i], ois[i]);
util_fmt_obj_end(); /* netdevs list */
util_fmt_obj_end(); /* adapter */
fflush(stdout);
}
if (ctx->opts.report) {
for (i = 0; i < zdev->num_netdevs; i++) {
rc = sclp_issue_optics_report(zdev, ois[i]);
if (rc == -ENOTSUP) {
fprintf(stderr, "Skipping %s which does not support reporting\n",
zdev->netdevs[i].name);
} else if (rc < 0) {
fprintf(stderr, "Error issuing SCLP for optics data failed: %s\n",
strerror(-rc));
}
}
}
free_ois:
for (i = 0; i < num_ois; i++)
optics_free(ois[i]);
free(ois);
return rc;
}
static void zpci_list_reload(struct util_list **zpci_list)
{
if (*zpci_list)
zpci_free_dev_list(*zpci_list);
*zpci_list = zpci_dev_list();
}
static void dump_all_adapter_data(struct opticsmon_ctx *ctx)
{
struct zpci_dev *zdev;
zpci_list_reload(&ctx->zpci_list);
util_list_iterate(ctx->zpci_list, zdev) {
/* Filter non-NIC devices and VFs */
if (zpci_is_vf(zdev) || !zdev->num_netdevs)
continue;
dump_adapter_data(ctx, zdev);
}
}
static int oneshot_mode(struct opticsmon_ctx *ctx)
{
util_fmt_init(stdout, FMT_JSON, FMT_DEFAULT, API_LEVEL);
if (!ctx->opts.quiet)
util_fmt_obj_start(FMT_LIST, "adapters");
dump_all_adapter_data(ctx);
if (!ctx->opts.quiet)
util_fmt_obj_end();
util_fmt_exit();
return EXIT_SUCCESS;
}
void on_link_change(struct zpci_netdev *netdev, void *arg)
{
struct opticsmon_ctx *ctx = arg;
struct zpci_netdev *found_netdev;
struct zpci_dev *zdev = NULL;
int reloads = 1;
do {
if (ctx->zpci_list) {
zdev = zpci_find_by_netdev(ctx->zpci_list, netdev->name, &found_netdev);
if (zdev) {
/* Skip data collection if operational state is
* unchanged
*/
if (found_netdev->operstate == netdev->operstate)
return;
/* Update operation state for VFs even though
* they are skipped just for a consistent view
*/
found_netdev->operstate = netdev->operstate;
/* Only collect optics data for PFs */
if (!zpci_is_vf(zdev))
dump_adapter_data(ctx, zdev);
return;
}
}
/* Could be uninitalized list or a new device, retry after reload */
zpci_list_reload(&ctx->zpci_list);
reloads--;
} while (reloads > 0);
}
#define MAX_EVENTS 8
static int monitor_wait_loop(struct opticsmon_ctx *ctx, int sigfd, int timerfd)
{
struct epoll_event events[MAX_EVENTS];
struct signalfd_siginfo fdsi;
int i, nlfd, epfd, nfds;
struct epoll_event ev;
uint64_t expirations;
ssize_t sread;
epfd = epoll_create1(EPOLL_CLOEXEC);
ev.events = EPOLLIN;
ev.data.fd = sigfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, sigfd, &ev) == -1)
return -EIO;
ev.events = EPOLLIN;
ev.data.fd = timerfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, timerfd, &ev) == -1)
return -EIO;
nlfd = link_mon_nl_waitfd_getfd(&ctx->lctx);
ev.events = EPOLLIN;
ev.data.fd = nlfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, nlfd, &ev) == -1)
return -EIO;
while (1) {
nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (nfds < 0)
return nfds;
for (i = 0; i < nfds; i++) {
/* signal fd */
if (events[i].data.fd == sigfd) {
sread = read(sigfd, &fdsi, sizeof(fdsi));
if (sread != sizeof(fdsi))
return -EIO;
switch (fdsi.ssi_signo) {
case SIGINT:
case SIGTERM:
case SIGQUIT:
return 0;
/* Unexpected signal */
default:
return -EIO;
}
/* timer fd */
} else if (events[i].data.fd == timerfd) {
sread = read(timerfd, &expirations, sizeof(uint64_t));
if (sread != sizeof(uint64_t))
return -EIO;
if (!expirations)
continue;
dump_all_adapter_data(ctx);
/* netlink fd */
} else if (events[i].data.fd == nlfd) {
link_mon_nl_waitfd_read(&ctx->lctx);
}
}
}
return 0;
}
static int monitor_mode(struct opticsmon_ctx *ctx)
{
struct itimerspec timerspec;
int sigfd, timerfd, ret;
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGQUIT);
sigaddset(&mask, SIGTERM);
if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1)
return -EIO;
sigfd = signalfd(-1, &mask, 0);
if (sigfd == -1) {
fprintf(stderr, "Failed to create signalfd\n");
return -EIO;
}
timerfd = timerfd_create(CLOCK_MONOTONIC, 0);
if (timerfd == -1) {
fprintf(stderr, "Failed to create timerfd\n");
ret = -EIO;
goto close_signalfd;
}
/* Set initial expiration to 1 ns so we gather optics data at startup */
timerspec.it_value.tv_sec = 0;
timerspec.it_value.tv_nsec = 1;
timerspec.it_interval.tv_sec = ctx->opts.interval_seconds;
timerspec.it_interval.tv_nsec = 0;
ret = timerfd_settime(timerfd, 0, &timerspec, NULL);
if (ret == -1) {
fprintf(stderr, "Failed to arm timer\n");
goto close_timerfd;
}
util_fmt_init(stdout, FMT_JSONSEQ, FMT_DEFAULT, API_LEVEL);
ret = link_mon_nl_waitfd_create(&ctx->lctx, on_link_change, ctx);
if (ret) {
fprintf(stderr, "Failed to create link monitoring socket\n");
goto close_timerfd;
}
monitor_wait_loop(ctx, sigfd, timerfd);
link_mon_nl_waitfd_destroy(&ctx->lctx);
util_fmt_exit();
close_signalfd:
close(sigfd);
close_timerfd:
close(timerfd);
return ret;
}
int main(int argc, char **argv)
{
struct opticsmon_ctx ctx = { .opts = { .interval_seconds = 86400 } };
int ret;
parse_cmdline(argc, argv, &ctx.opts);
ethtool_nl_connect(&ctx.ethtool_ctx);
if (ctx.opts.monitor)
ret = monitor_mode(&ctx);
else
ret = oneshot_mode(&ctx);
ethtool_nl_close(&ctx.ethtool_ctx);
if (ctx.zpci_list)
zpci_free_dev_list(ctx.zpci_list);
return ret;
}

12
rust/Cargo.lock generated
View File

@@ -1222,9 +1222,21 @@ dependencies = [
"clap",
"libc",
"log",
"openssl",
"s390_pv",
"serde",
"serde_json",
"utils_macros",
]
[[package]]
name = "utils_macros"
version = "0.12.0"
dependencies = [
"clap",
"proc-macro2",
"quote",
"syn",
]
[[package]]

View File

@@ -11,6 +11,7 @@ members = [
"pvsecret",
"pvverify",
"utils",
"utils_macros",
]
resolver = "2"

View File

@@ -17,6 +17,8 @@ use crate::{assert_size, request::MagicValue, static_assert, Error, Result, PAGE
pub enum SeHdrVersion {
/// Secure Execution header v1
One = 0x100,
/// Secure Execution header v2
Two = 0x200,
}
/// Struct containing all SE-header tags.
@@ -241,6 +243,7 @@ impl BootHdrTags {
// Some sanity checks
let hdr_version = match hdr_head.version.get() {
0x100 => SeHdrVersion::One,
0x200 => SeHdrVersion::Two,
_ => {
debug!("Unsupported hdr-version: {:0>4x}", hdr_head.version.get());
return Err(Error::InvBootHdr);

View File

@@ -7,12 +7,14 @@ use std::fmt::Display;
use std::ops::Range;
use enum_dispatch::enum_dispatch;
use openssl::bn::BigNumContext;
use openssl::derive::Deriver;
use openssl::ec::{EcGroup, EcKey};
use openssl::ec::{EcGroup, EcKey, EcPoint};
use openssl::error::ErrorStack;
use openssl::hash::{DigestBytes, MessageDigest};
use openssl::md::MdRef;
use openssl::nid::Nid;
use openssl::pkey::{HasPublic, Id, PKey, PKeyRef, Private, Public};
use openssl::pkey::{HasPublic, Id, KeyType, PKey, PKeyRef, Private, Public};
use openssl::pkey_ctx::{HkdfMode, PkeyCtx};
use openssl::rand::rand_bytes;
use openssl::rsa::Padding;
@@ -23,8 +25,51 @@ use openssl::symm::{
use pv_core::request::Confidential;
use crate::error::Result;
use crate::openssl_extensions::PkeyEncapsulateContext;
use crate::req::get_pub_ecdh_points;
use crate::request::EcPubKeyCoord;
use crate::Error;
/// Compute ECDH shared secret from public and private keys
///
/// It is expected that the public and private key are with respect to EC-P521.
///
/// Note that the output is the concatenation of the 80-bytes-left-padded x and
/// 80-bytes-left-padded y coordinate.
fn ecdh_shared_secret(
pub_key: &PKeyRef<Public>,
priv_key: &PKeyRef<Private>,
) -> Result<[u8; 160], ErrorStack> {
let pub_key = pub_key.ec_key()?;
let priv_key = priv_key.ec_key()?;
// Verify both keys use the EC-P521 curve (SECP521R1)
assert_eq!(
pub_key.group().curve_name(),
Some(Nid::SECP521R1),
"Public key must use EC-P521 curve"
);
assert_eq!(
priv_key.group().curve_name(),
Some(Nid::SECP521R1),
"Private key must use EC-P521 curve"
);
pub_key.check_key()?;
priv_key.check_key()?;
let group = pub_key.group();
let mut bn_ctx = BigNumContext::new()?;
let mut point = EcPoint::new(group)?;
point.mul2(
group,
pub_key.public_key(),
priv_key.private_key(),
&mut bn_ctx,
)?;
let coord = get_pub_ecdh_points(&point, group)?;
Ok(coord)
}
/// An AES256-GCM key that will purge itself out of the memory when going out of scope
pub type Aes256GcmKey = Confidential<[u8; SymKeyType::AES_256_GCM_KEY_LEN]>;
/// An AES256-XTS key that will purge itself out of the memory when going out of scope
@@ -74,6 +119,7 @@ impl SymKeyType {
}
/// Returns true if the [`SymKeyType`] is an AEAD key
#[must_use]
pub const fn is_aead(&self) -> bool {
self.tag_len().is_some()
}
@@ -214,6 +260,220 @@ pub fn derive_aes256_gcm_key(k1: &PKeyRef<Private>, k2: &PKeyRef<Public>) -> Res
))
}
/// Determines the KeyType of a given PKey by testing against all known key types.
///
/// This function iterates through all known OpenSSL key types and uses the `is_a()`
/// method to identify which type the provided key matches. This is more reliable than
/// using `Id` to `KeyType` conversion, especially for newer key types like ML-KEM
/// that may not have a direct `Id` mapping.
///
/// # Parameters
///
/// * `key` - A reference to the PKey to identify
///
/// # Returns
///
/// * `Some(KeyType)` - If the key matches one of the known key types
/// * `None` - If the key type is not recognized or doesn't match any known types
fn pkey_to_keytype<T>(key: &PKeyRef<T>) -> Option<KeyType> {
const KNOWN_KEY_TYPES: &[KeyType] = &[
KeyType::RSA,
KeyType::RSA_PSS,
KeyType::DSA,
KeyType::DH,
KeyType::EC,
KeyType::HMAC,
KeyType::CMAC,
KeyType::X25519,
KeyType::ED25519,
KeyType::X448,
KeyType::ED448,
KeyType::ML_KEM_512,
KeyType::ML_KEM_768,
KeyType::ML_KEM_1024,
];
KNOWN_KEY_TYPES
.iter()
.find(|&&key_type| key.is_a(key_type))
.copied()
}
fn key_type_str(t: KeyType) -> &'static str {
if t == KeyType::RSA {
"RSA"
} else if t == KeyType::RSA_PSS {
"RSA-PSS"
} else if t == KeyType::DSA {
"DSA"
} else if t == KeyType::DH {
"DH"
} else if t == KeyType::EC {
"EC"
} else if t == KeyType::HMAC {
"HMAC"
} else if t == KeyType::CMAC {
"CMAC"
} else if t == KeyType::X25519 {
"X25519"
} else if t == KeyType::ED25519 {
"ED25519"
} else if t == KeyType::X448 {
"X448"
} else if t == KeyType::ED448 {
"ED448"
} else if t == KeyType::ML_KEM_512 {
"ML-KEM-512"
} else if t == KeyType::ML_KEM_768 {
"ML-KEM-768"
} else if t == KeyType::ML_KEM_1024 {
"ML-KEM-1024"
} else {
"unknown"
}
}
/// Validates that a key matches the expected key type.
///
/// # Errors
///
/// Returns an error if the key doesn't match the expected type.
pub(crate) fn validate_key_type<T: HasPublic>(
key: &PKeyRef<T>,
key_name: &str,
expected_type: KeyType,
) -> Result<()> {
if !key.is_a(expected_type) {
return Err(Error::RetrInvKey {
what: "key type",
kind: key_name.to_string(),
value: pkey_to_keytype(key)
.map(key_type_str)
.unwrap_or("unknown")
.to_string(),
exp: key_type_str(expected_type).to_string(),
});
}
Ok(())
}
/// Validates that a key is an EC key with the specified curve.
///
/// # Errors
///
/// Returns an error if the key is not an EC key or doesn't use the expected curve.
pub(crate) fn validate_ec_key<T: HasPublic>(
key: &PKeyRef<T>,
key_name: &str,
expected_curve: Nid,
) -> Result<()> {
if key.id() != Id::EC {
return Err(Error::RetrInvKey {
what: "key type",
kind: key_name.to_string(),
value: pkey_to_keytype(key)
.map(key_type_str)
.unwrap_or("unknown")
.to_string(),
exp: format!("EC ({})", expected_curve.long_name().unwrap_or("unknown")),
});
}
let ec_key = key.ec_key()?;
if ec_key.group().curve_name() != Some(expected_curve) {
return Err(Error::RetrInvKey {
what: "curve",
kind: key_name.to_string(),
value: ec_key
.group()
.curve_name()
.and_then(|nid| nid.long_name().ok())
.unwrap_or("unknown")
.to_string(),
exp: expected_curve.long_name().unwrap_or("unknown").to_string(),
});
}
Ok(())
}
/// Derive a symmetric AES 256 GCM key from a private target key, a public
/// customer key, and a public ML-KEM target key.
///
/// # Returns
///
/// The derived key and the ML-KEM ciphertext (KC).
///
/// # Errors
///
/// This function will return an error if something went bad in OpenSSL or the
/// wrong key types were used.
pub fn derive_aes256_gcm_key_hybrid(
priv_ecdh_cust_key: &PKeyRef<Private>,
pub_ecdh_target_key: &PKeyRef<Public>,
pub_mlkem_target_key: &PKeyRef<Public>,
) -> Result<(Aes256GcmKey, Vec<u8>)> {
let mut buffer: Vec<u8> = vec![0, 0, 0, 1];
validate_ec_key(priv_ecdh_cust_key, "ECDH customer key", Nid::SECP521R1)?;
validate_ec_key(pub_ecdh_target_key, "ECDH target key", Nid::SECP521R1)?;
validate_key_type(
pub_mlkem_target_key,
"ML-KEM target key",
KeyType::ML_KEM_1024,
)?;
// Derive the ECDH shared secret
let ecdh_derived_secret = ecdh_shared_secret(pub_ecdh_target_key, priv_ecdh_cust_key)?;
assert_eq!(ecdh_derived_secret.as_ref().len(), 160);
buffer.extend_from_slice(ecdh_derived_secret.as_ref());
// Derive the ML-KEM shared secret
let mut ctx = PkeyCtx::new(pub_mlkem_target_key)?;
ctx.encapsulate_init()?;
let (mut ciphertext, mut shared_secret) = (vec![], vec![]);
ctx.encapsulate_to_vec(&mut ciphertext, &mut shared_secret)?;
assert_eq!(ciphertext.len(), 1568);
assert_eq!(shared_secret.len(), 32);
buffer.extend_from_slice(&shared_secret);
// Append the private ECDH customer key
let pub_ecdh_cust_key = EcPubKeyCoord::try_from(priv_ecdh_cust_key)?;
assert_eq!(pub_ecdh_cust_key.as_ref().len(), 160);
buffer.extend_from_slice(pub_ecdh_cust_key.as_ref());
// Append the ciphertext
buffer.extend_from_slice(&ciphertext);
// Append the public ECDH target key
let pub_ecdh_target_key: EcPubKeyCoord = pub_ecdh_target_key.try_into()?;
assert_eq!(pub_ecdh_target_key.as_ref().len(), 160);
buffer.extend_from_slice(pub_ecdh_target_key.as_ref());
// Append the public ML-KEM target key
assert_eq!(pub_mlkem_target_key.raw_public_key()?.len(), 1568);
buffer.extend_from_slice(&pub_mlkem_target_key.raw_public_key()?);
// Append the magic string
const STRING: &str = "PQC Secure Execution with Format-2 Key Slots KS2";
assert_eq!(STRING.len(), 48);
buffer.extend_from_slice(STRING.as_bytes());
// Sanity check
assert_eq!(buffer.len(), 4 + 160 + 32 + 160 + 1568 + 160 + 1568 + 48);
let secr = Confidential::new(buffer);
// Panic: does not panic as SHA256 digest is 32 bytes long
Ok((
Aes256GcmKey::new(
hash(MessageDigest::sha256(), secr.value())?
.as_ref()
.try_into()
.unwrap(),
),
ciphertext,
))
}
/// Generate a random array.
///
/// # Errors
@@ -760,6 +1020,112 @@ mod tests {
assert_eq!(&calc_key, &exp_key);
}
#[test]
fn derive_aes256_gcm_key_hybrid() {
let (cust_key, host_key_1, host_key_2) = get_test_keys_hybrid();
let entropy = [0x5au8; 4096];
let nonce = [0xa5u8; 48];
let _rng = DeterministicTestRandGuard::install(&entropy, &nonce).unwrap();
let exp_key: Aes256GcmKey = [
197, 167, 157, 112, 186, 112, 72, 125, 192, 219, 168, 132, 178, 167, 249, 123, 149, 3,
151, 166, 162, 66, 120, 39, 41, 230, 143, 54, 172, 10, 200, 143,
]
.into();
let exp_kc = [
54, 117, 96, 77, 148, 147, 170, 100, 34, 177, 95, 7, 35, 243, 145, 115, 7, 87, 178, 9,
169, 99, 193, 99, 244, 195, 23, 78, 11, 153, 221, 196, 5, 192, 253, 192, 86, 49, 194,
236, 43, 69, 183, 125, 166, 87, 158, 188, 13, 152, 19, 6, 253, 29, 194, 0, 101, 236,
28, 171, 3, 236, 53, 186, 191, 109, 7, 83, 220, 93, 126, 29, 19, 203, 201, 39, 59, 7,
131, 51, 81, 73, 254, 69, 105, 185, 214, 179, 155, 194, 189, 122, 106, 130, 249, 48, 4,
33, 245, 170, 163, 4, 223, 208, 138, 224, 203, 119, 105, 59, 187, 153, 235, 90, 79,
127, 29, 136, 230, 142, 78, 83, 27, 131, 58, 126, 76, 53, 129, 20, 85, 108, 86, 64,
244, 90, 84, 177, 239, 105, 90, 41, 118, 189, 88, 174, 224, 216, 29, 10, 123, 81, 212,
203, 197, 120, 20, 190, 3, 45, 37, 194, 208, 249, 232, 221, 67, 10, 62, 121, 143, 169,
227, 165, 17, 30, 85, 223, 44, 141, 114, 142, 105, 119, 187, 41, 46, 8, 6, 17, 29, 165,
117, 254, 92, 174, 231, 25, 117, 69, 112, 216, 80, 73, 185, 54, 50, 119, 145, 220, 174,
26, 105, 81, 114, 210, 144, 148, 109, 218, 64, 78, 231, 196, 229, 88, 46, 128, 106,
125, 204, 58, 184, 127, 193, 207, 86, 163, 98, 164, 57, 242, 29, 59, 251, 227, 185, 60,
18, 68, 74, 47, 203, 61, 164, 78, 245, 100, 87, 148, 210, 97, 158, 252, 79, 78, 50,
143, 35, 231, 215, 211, 75, 133, 214, 227, 140, 27, 21, 46, 221, 84, 89, 165, 161, 227,
46, 117, 193, 254, 190, 237, 130, 28, 57, 52, 14, 235, 154, 115, 172, 185, 67, 116, 34,
242, 158, 209, 0, 126, 196, 93, 224, 29, 246, 145, 65, 73, 185, 196, 4, 107, 124, 241,
157, 230, 168, 244, 238, 84, 188, 173, 17, 238, 26, 161, 24, 176, 229, 226, 33, 244,
167, 41, 107, 156, 29, 226, 248, 64, 146, 191, 210, 234, 76, 144, 219, 92, 136, 173,
241, 98, 0, 71, 135, 214, 196, 116, 63, 243, 73, 71, 130, 171, 86, 204, 149, 69, 164,
20, 177, 122, 95, 226, 95, 126, 106, 160, 59, 97, 137, 8, 73, 113, 189, 172, 24, 114,
60, 62, 249, 193, 3, 99, 34, 153, 42, 238, 77, 181, 80, 185, 223, 39, 8, 44, 215, 119,
214, 30, 136, 19, 215, 35, 184, 69, 94, 10, 170, 179, 51, 183, 105, 237, 237, 48, 199,
122, 159, 87, 183, 71, 230, 87, 102, 77, 81, 116, 28, 126, 195, 72, 50, 157, 223, 243,
83, 36, 16, 168, 111, 209, 132, 12, 96, 56, 140, 57, 144, 75, 253, 119, 123, 168, 2,
79, 214, 121, 80, 154, 93, 235, 222, 130, 181, 166, 97, 51, 106, 21, 138, 224, 8, 144,
223, 162, 152, 183, 6, 80, 64, 144, 21, 155, 56, 255, 108, 248, 125, 196, 46, 99, 119,
94, 104, 63, 46, 15, 165, 30, 98, 75, 212, 193, 116, 151, 189, 65, 42, 83, 253, 183,
41, 195, 45, 206, 178, 66, 36, 215, 197, 105, 236, 79, 91, 135, 164, 71, 187, 199, 200,
150, 226, 182, 254, 6, 234, 109, 3, 17, 116, 249, 44, 211, 184, 61, 189, 44, 181, 249,
8, 58, 230, 236, 8, 188, 14, 178, 100, 120, 250, 29, 1, 204, 158, 46, 161, 39, 66, 76,
42, 114, 149, 160, 31, 87, 254, 181, 224, 17, 162, 163, 99, 11, 34, 149, 50, 203, 205,
224, 38, 18, 233, 161, 49, 7, 151, 63, 81, 68, 71, 174, 49, 22, 143, 93, 50, 0, 154,
152, 178, 134, 147, 152, 118, 196, 241, 233, 67, 102, 149, 179, 213, 176, 118, 64, 172,
143, 134, 196, 232, 154, 110, 129, 155, 159, 103, 117, 202, 11, 35, 75, 104, 5, 11,
160, 147, 174, 49, 248, 45, 247, 16, 7, 64, 209, 255, 170, 243, 242, 40, 158, 94, 239,
194, 225, 113, 24, 90, 243, 73, 137, 217, 175, 130, 50, 133, 139, 250, 145, 190, 76,
151, 183, 30, 86, 146, 59, 171, 214, 211, 135, 203, 192, 42, 189, 90, 47, 152, 132,
168, 252, 175, 71, 234, 118, 207, 161, 176, 254, 189, 54, 174, 160, 178, 158, 133, 122,
63, 75, 95, 201, 55, 139, 2, 208, 232, 110, 74, 201, 196, 135, 244, 156, 87, 208, 101,
203, 121, 187, 16, 106, 80, 120, 165, 44, 147, 182, 114, 173, 186, 185, 255, 99, 85,
88, 26, 27, 43, 203, 176, 207, 88, 20, 253, 169, 210, 168, 109, 75, 234, 239, 8, 243,
244, 65, 164, 193, 255, 240, 215, 54, 158, 188, 93, 93, 54, 46, 77, 152, 78, 174, 154,
67, 248, 24, 235, 172, 240, 83, 224, 17, 100, 217, 15, 172, 176, 46, 85, 107, 105, 127,
147, 158, 202, 255, 145, 237, 84, 223, 100, 214, 38, 133, 169, 112, 227, 138, 220, 125,
72, 197, 5, 227, 94, 245, 42, 70, 33, 209, 243, 70, 229, 37, 118, 214, 147, 43, 87, 39,
241, 107, 26, 169, 28, 72, 223, 133, 145, 44, 248, 213, 52, 127, 250, 99, 193, 115,
113, 147, 89, 112, 237, 199, 208, 36, 155, 106, 144, 73, 249, 8, 116, 198, 107, 120,
233, 145, 11, 155, 178, 7, 66, 157, 255, 206, 128, 155, 233, 111, 148, 194, 214, 238,
252, 230, 96, 119, 30, 37, 73, 133, 129, 87, 185, 149, 251, 156, 17, 8, 83, 106, 207,
98, 203, 100, 39, 199, 127, 253, 59, 37, 121, 161, 216, 146, 6, 178, 183, 243, 191, 91,
106, 243, 132, 111, 216, 163, 87, 210, 197, 173, 146, 65, 131, 194, 96, 70, 6, 7, 192,
45, 173, 71, 44, 134, 122, 60, 173, 208, 238, 22, 187, 208, 212, 51, 191, 185, 174, 3,
125, 28, 134, 216, 209, 4, 224, 199, 16, 15, 56, 70, 188, 216, 92, 24, 96, 57, 125,
138, 151, 73, 254, 245, 106, 53, 4, 150, 74, 43, 42, 4, 157, 238, 125, 168, 41, 224,
22, 249, 45, 117, 32, 180, 161, 41, 39, 180, 96, 24, 2, 102, 57, 116, 34, 75, 90, 72,
134, 176, 2, 196, 59, 143, 182, 201, 117, 178, 153, 81, 108, 167, 122, 139, 71, 197,
55, 114, 60, 161, 130, 14, 29, 79, 152, 55, 136, 62, 190, 228, 202, 53, 126, 4, 173,
99, 28, 190, 224, 255, 134, 123, 166, 162, 244, 55, 26, 81, 120, 207, 193, 10, 103,
153, 215, 220, 12, 71, 67, 217, 154, 212, 44, 200, 232, 0, 178, 39, 44, 22, 7, 14, 215,
183, 192, 104, 51, 46, 93, 102, 195, 65, 9, 191, 241, 237, 151, 5, 64, 103, 228, 162,
41, 123, 29, 5, 80, 203, 198, 234, 230, 107, 53, 60, 58, 253, 47, 152, 22, 77, 81, 86,
215, 132, 152, 135, 6, 218, 46, 92, 192, 218, 198, 234, 76, 178, 25, 203, 48, 61, 76,
215, 96, 6, 49, 195, 37, 225, 10, 175, 222, 186, 133, 63, 50, 236, 215, 247, 17, 199,
8, 134, 64, 246, 194, 167, 105, 15, 57, 62, 50, 51, 243, 192, 242, 122, 11, 46, 202,
47, 10, 71, 153, 212, 226, 38, 12, 90, 150, 154, 152, 233, 7, 172, 111, 185, 160, 246,
0, 166, 113, 90, 37, 203, 166, 43, 53, 255, 211, 127, 139, 73, 7, 10, 164, 1, 168, 223,
87, 127, 43, 47, 87, 68, 84, 247, 223, 108, 108, 113, 36, 17, 50, 98, 236, 48, 10, 219,
182, 107, 240, 198, 207, 20, 178, 9, 142, 14, 93, 163, 166, 147, 38, 176, 172, 156, 73,
174, 238, 175, 231, 130, 159, 51, 128, 76, 34, 37, 138, 19, 3, 59, 71, 78, 144, 238,
226, 214, 188, 27, 42, 142, 245, 238, 131, 190, 211, 240, 41, 122, 69, 124, 171, 75,
115, 45, 144, 133, 176, 19, 81, 125, 230, 149, 235, 159, 6, 155, 195, 119, 62, 140, 50,
52, 209, 124, 3, 93, 232, 20, 130, 138, 110, 60, 183, 177, 161, 52, 114, 91, 19, 211,
156, 185, 202, 200, 36, 103, 253, 113, 45, 245, 177, 238, 43, 144, 38, 221, 0, 102, 50,
255, 20, 154, 56, 156, 155, 92, 157, 57, 209, 77, 84, 88, 24, 116, 116, 54, 213, 222,
76, 212, 193, 168, 216, 247, 125, 135, 114, 226, 128, 140, 250, 103, 82, 215, 238, 32,
74, 252, 45, 224, 23, 95, 126, 124, 135, 124, 128, 53, 203, 40, 65, 222, 8, 83, 178,
211, 64, 141, 64, 98, 188, 134, 100, 65, 166, 52, 249, 1, 206, 58, 55, 195, 23, 218,
239, 41, 73, 88, 113, 148, 132, 209, 93, 37, 205, 58, 92, 14, 1, 133, 168, 162, 192,
147, 70, 167, 101, 170, 152, 159, 0, 212, 26, 97, 49, 43, 217, 173, 38, 215, 136, 26,
208, 244, 19, 83, 207, 38, 224, 254, 92, 169, 219, 236, 172, 49, 55, 98, 55, 15, 187,
173, 114, 99, 130, 211, 78, 168, 221, 209, 250, 88, 189, 17, 186, 172, 129, 56, 90,
238, 120, 23, 176, 87, 133, 81, 244, 29, 2, 215, 34, 88, 247, 231, 167, 56,
];
let (exc_key, kc) =
super::derive_aes256_gcm_key_hybrid(&cust_key, &host_key_1, &host_key_2).unwrap();
assert_eq!(exc_key, exp_key);
assert_eq!(kc, exp_kc);
}
#[test]
fn hkdf_rfc_5869() {
use openssl::md::Md;
@@ -933,4 +1299,94 @@ mod tests {
}))
));
}
#[test]
fn validate_ec_key_valid() {
let (cust_key, host_key) = get_test_keys();
// Both test keys are SECP521R1 EC keys
assert!(validate_ec_key(&cust_key, "customer key", Nid::SECP521R1).is_ok());
assert!(validate_ec_key(&host_key, "host key", Nid::SECP521R1).is_ok());
}
#[test]
fn validate_ec_key_wrong_curve() {
let (cust_key, _) = get_test_keys();
// Test key is SECP521R1, but we expect SECP384R1
let result = validate_ec_key(&cust_key, "customer key", Nid::SECP384R1);
assert!(result.is_err());
if let Err(Error::RetrInvKey {
what,
kind,
value,
exp,
}) = result
{
assert_eq!(what, "curve");
assert_eq!(kind, "customer key");
assert_eq!(value, "secp521r1");
assert_eq!(exp, "secp384r1");
} else {
panic!("Expected RetrInvKey error");
}
}
#[test]
fn validate_ec_key_not_ec() {
let keypair = crate::get_test_asset!("keys/rsa2048key.pem");
let keypair = PKey::private_key_from_pem(keypair).unwrap();
// RSA key is not an EC key
let result = validate_ec_key(&keypair, "EC key", Nid::SECP521R1);
assert!(result.is_err());
if let Err(Error::RetrInvKey {
what,
kind,
value,
exp,
}) = result
{
assert_eq!(what, "key type");
assert_eq!(kind, "EC key");
assert_eq!(value, "RSA");
assert_eq!(exp, "EC (secp521r1)");
} else {
panic!("Expected RetrInvKey error");
}
}
#[test]
fn validate_mlkem_key_valid() {
let (_, _, mlkem_key) = get_test_keys_hybrid();
// The third key from get_test_keys_hybrid is ML-KEM-1024
assert!(validate_key_type(&mlkem_key, "ML-KEM key", KeyType::ML_KEM_1024).is_ok());
}
#[test]
fn validate_mlkem_key_wrong_type() {
let (ec_key, _) = get_test_keys();
// EC key is not ML-KEM
let result = validate_key_type(&ec_key, "EC key", KeyType::ML_KEM_1024);
assert!(result.is_err());
if let Err(Error::RetrInvKey {
what,
kind,
value,
exp,
}) = result
{
assert_eq!(what, "key type");
assert_eq!(kind, "EC key");
assert_eq!(value, "EC");
assert_eq!(exp, "ML-KEM-1024");
} else {
panic!("Expected RetrInvKey error");
}
}
}

View File

@@ -64,8 +64,11 @@ pub enum Error {
#[error("No user-key for verification provided and user-data is signed")]
BinAsrcbNoUserDataSgnKey,
#[error("Input does not contain an add-secret request version 1")]
BinAsrcbInvVersion,
#[error("Input contains an unknown add-secret request version {0}")]
BinAsrcbInvVersion(u32),
#[error("Unsupported add-secret request version: {0}")]
UnsupportedAddSecretVersion(u32),
#[error("Provided user-data key type ({key}) does not match with the user-data ({kind})")]
AsrcbUserDataKeyMismatch { key: String, kind: UserDataType },
@@ -84,6 +87,9 @@ pub enum Error {
#[error("The provided host key document in {0} contains no certificate!")]
NoHkdInFile(String),
#[error("The provided host key document in {0} contains the wrong number of keys!")]
WrongNumberOfKeys(String),
#[error("Invalid input size ({0}) for boot hdr")]
InvBootHdrSize(usize),
@@ -135,6 +141,12 @@ pub enum Error {
#[error("Unsupported cipher: {:?}", .0.as_raw())]
UnsupportedCipher(Nid),
#[error("{}", .0)]
InvalidHkd(String),
#[error("All host keys must use the same version (all hybrid or all non-hybrid)")]
MixedHostkeyVersions,
}
// used in macros

View File

@@ -89,13 +89,16 @@ pub use crate::error::HkdVerifyErrorType;
/// Functionalities to build UV requests
pub mod request {
pub use crate::brcb::{seek_se_hdr_start, BootHdrTags, SeImgMetaData};
pub use crate::brcb::{seek_se_hdr_start, BootHdrTags, SeHdrVersion, SeImgMetaData};
pub use crate::crypto::{
decrypt_aead, derive_aes256_gcm_key, encrypt_aead, gen_ec_key, random_array,
AeadDecryptionResult, AeadEncryptionResult, Aes256GcmKey, Aes256XtsKey, SymKey, SymKeyType,
SHA_512_HASH_LEN,
};
pub use crate::req::{EcPubKeyCoord, Encrypt, HostKey, Keyslot, ReqEncrCtx, Request};
pub use crate::req::{
EcPubKeyCoord, Encrypt, HostKey, HybridPKey, Keyslot, KeyslotV1, KeyslotV2, ReqEncrCtx,
Request,
};
pub use crate::verify::{CertVerifier, HkdVerifier, NoVerifyHkd};
/// Reexports some useful OpenSSL symbols

View File

@@ -14,5 +14,4 @@ mod stackable_crl;
pub use akid::*;
pub use bio::*;
pub use crl::*;
#[expect(unused_imports)]
pub use ml_kem::*;

View File

@@ -47,7 +47,10 @@ impl EcPubKeyCoord {
/// Get the pub ECDH coordinates in the format the Ultravisor expects it:
/// The two coordinates are padded to 80 bytes each.
fn get_pub_ecdh_points(pkey: &EcPointRef, grp: &EcGroupRef) -> Result<[u8; 160], ErrorStack> {
pub(crate) fn get_pub_ecdh_points(
pkey: &EcPointRef,
grp: &EcGroupRef,
) -> Result<[u8; 160], ErrorStack> {
let mut x = BigNum::new()?;
let mut y = BigNum::new()?;
let mut bn_ctx = BigNumContext::new()?;

View File

@@ -3,6 +3,8 @@
// Copyright IBM Corp.
//! Request header structure for UV requests.
use pv_core::request::{RequestMagic, RequestVersion};
use zerocopy::{BigEndian, FromBytes, Immutable, IntoBytes, U32};
use crate::assert_size;
@@ -25,13 +27,13 @@ pub struct RequestHdr {
assert_size!(RequestHdr, 48);
impl RequestHdr {
pub(crate) fn new(
rqvn: u32,
pub(super) fn new(
rqvn: RequestVersion,
rql: u32,
iv: [u8; 12],
nks: u8,
sea: u32,
magic: Option<[u8; 8]>,
magic: Option<RequestMagic>,
) -> Self {
Self {
magic: magic.unwrap_or_default(),

View File

@@ -4,7 +4,56 @@
//! Host key types for UV requests
use openssl::pkey::{PKey, Public};
use openssl::nid::Nid;
use openssl::pkey::{KeyType, PKey, PKeyRef, Public};
use crate::crypto::{validate_ec_key, validate_key_type};
pub use crate::error::Result;
/// Hybrid public key (ECDH and ML-KEM)
#[derive(Clone, Debug)]
pub struct HybridPKey {
/// ECDH public key
pub(super) ec_key: PKey<Public>,
/// ML-KEM public key
pub(super) mlkem_key: PKey<Public>,
}
impl HybridPKey {
/// Creates a new hybrid public key with validation.
///
/// # Parameters
/// - `ec_key`: ECDH public key (must be SECP521R1)
/// - `mlkem_key`: ML-KEM public key (must be ML-KEM-1024)
///
/// # Errors
/// Returns an error if:
/// - EC key is not SECP521R1 curve
/// - ML-KEM key is not ML-KEM-1024
pub fn new(ec_key: PKey<Public>, mlkem_key: PKey<Public>) -> Result<Self> {
validate_ec_key(&ec_key, "ECDH key", Nid::SECP521R1)?;
validate_key_type(&mlkem_key, "ML-KEM key", KeyType::ML_KEM_1024)?;
Ok(Self { ec_key, mlkem_key })
}
/// Returns a reference to the EC key
pub fn ec_key(&self) -> &PKeyRef<Public> {
&self.ec_key
}
/// Returns a reference to the ML-KEM key
pub fn mlkem_key(&self) -> &PKeyRef<Public> {
&self.mlkem_key
}
}
impl AsRef<HybridPKey> for HybridPKey {
fn as_ref(&self) -> &HybridPKey {
self
}
}
/// Versioned host keys container
#[non_exhaustive]
@@ -12,15 +61,33 @@ use openssl::pkey::{PKey, Public};
pub enum HostKey {
/// ECDH public key
V1(PKey<Public>),
/// Hybrid public key (ECDH and ML-KEM)
V2(HybridPKey),
}
impl HostKey {
/// Return the ECDH public key
pub fn ec_key(&self) -> &PKey<Public> {
match self {
pub fn ec_key(&self) -> Option<&PKeyRef<Public>> {
Some(match self {
HostKey::V1(ec_key) => ec_key,
HostKey::V2(hybrid) => hybrid.ec_key(),
})
}
/// Return the ML-KEM public key
pub fn mlkem_key(&self) -> Option<&PKeyRef<Public>> {
match self {
HostKey::V1(_) => None,
HostKey::V2(hybrid) => Some(hybrid.mlkem_key()),
}
}
/// Test if the hostkey is hybrid
#[must_use]
pub fn is_hybrid(&self) -> bool {
matches!(self, HostKey::V2(_))
}
}
impl AsRef<HostKey> for HostKey {
@@ -28,3 +95,248 @@ impl AsRef<HostKey> for HostKey {
self
}
}
#[cfg(test)]
mod tests {
use openssl::ec::{EcGroup, EcKey};
use openssl::pkey::Private;
use super::*;
use crate::openssl_extensions::generate_ml_kem;
use crate::test_utils::get_test_key_and_cert_hybrid;
use crate::Error;
fn to_public_key(key: &PKey<Private>) -> Result<PKey<Public>> {
let der = key.public_key_to_der()?;
Ok(PKey::public_key_from_der(&der)?)
}
#[test]
fn test_hostkey_v1_variant() {
let (_, ec_key) = crate::test_utils::get_test_key_and_cert();
let hostkey = HostKey::V1(ec_key.public_key().unwrap());
assert!(!hostkey.is_hybrid(), "V1 HostKey should not be hybrid");
assert!(matches!(hostkey, HostKey::V1(_)));
}
#[test]
fn test_hostkey_v2_variant() {
let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid();
let hybrid = HybridPKey::new(
ec_key.public_key().unwrap(),
mlkem_key.public_key().unwrap(),
)
.unwrap();
let hostkey = HostKey::V2(hybrid);
assert!(hostkey.is_hybrid(), "V2 HostKey should be hybrid");
assert!(matches!(hostkey, HostKey::V2(_)));
}
#[test]
fn test_hostkey_ec_key_access() {
let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid();
let hybrid = HybridPKey::new(
ec_key.public_key().unwrap(),
mlkem_key.public_key().unwrap(),
)
.unwrap();
let v1_key = HostKey::V1(ec_key.public_key().unwrap());
let v2_key = HostKey::V2(hybrid);
assert!(v1_key.ec_key().unwrap().public_key_to_der().is_ok());
assert!(v2_key.ec_key().unwrap().public_key_to_der().is_ok());
}
#[test]
fn test_hostkey_v2_mlkem_key_access() {
let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid();
let hybrid = HybridPKey::new(
ec_key.public_key().unwrap(),
mlkem_key.public_key().unwrap(),
)
.unwrap();
let hostkey = HostKey::V2(hybrid);
assert!(hostkey.mlkem_key().unwrap().public_key_to_der().is_ok());
}
#[test]
fn test_hostkey_v1_has_no_mlkem_key() {
let (_, ec_key) = crate::test_utils::get_test_key_and_cert();
let hostkey = HostKey::V1(ec_key.public_key().unwrap());
assert!(hostkey.mlkem_key().is_none());
}
#[test]
fn test_hybrid_public_key_structure() {
let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid();
let hybrid = HybridPKey::new(
ec_key.public_key().unwrap(),
mlkem_key.public_key().unwrap(),
)
.unwrap();
// Verify both components are present and valid
assert!(!hybrid.ec_key().public_key_to_der().unwrap().is_empty());
assert!(!hybrid.mlkem_key().public_key_to_der().unwrap().is_empty());
}
#[test]
fn test_hybrid_pkey_invalid_ec_curve() {
// Generate a P-256 key instead of P-521
let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap();
let wrong_ec_key = PKey::from_ec_key(EcKey::generate(&group).unwrap()).unwrap();
let (_, _, mlkem_key) = get_test_key_and_cert_hybrid();
let result = HybridPKey::new(
to_public_key(&wrong_ec_key).unwrap(),
mlkem_key.public_key().unwrap(),
);
assert!(result.is_err(), "Should reject EC key with wrong curve");
if let Err(Error::RetrInvKey {
what,
kind,
value,
exp,
}) = result
{
assert_eq!(what, "curve");
assert_eq!(kind, "ECDH key");
assert_eq!(value, "prime256v1");
assert_eq!(exp, "secp521r1");
} else {
panic!("Expected RetrInvKey error for wrong curve");
}
}
#[test]
fn test_hybrid_pkey_invalid_mlkem_type() {
let (_, ec_key, _) = get_test_key_and_cert_hybrid();
// Generate ML-KEM-512 instead of ML-KEM-1024
let wrong_mlkem_key = generate_ml_kem(KeyType::ML_KEM_512).unwrap();
let result = HybridPKey::new(
ec_key.public_key().unwrap(),
to_public_key(&wrong_mlkem_key).unwrap(),
);
assert!(result.is_err(), "Should reject ML-KEM key with wrong type");
if let Err(Error::RetrInvKey {
what,
kind,
value,
exp,
}) = result
{
assert_eq!(what, "key type");
assert_eq!(kind, "ML-KEM key");
assert_eq!(value, "ML-KEM-512");
assert_eq!(exp, "ML-KEM-1024");
} else {
panic!("Expected RetrInvKey error for wrong ML-KEM type");
}
}
#[test]
fn test_hybrid_pkey_clone() {
let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid();
let hybrid = HybridPKey::new(
ec_key.public_key().unwrap(),
mlkem_key.public_key().unwrap(),
)
.unwrap();
let cloned = hybrid.clone();
// Verify both original and clone have valid keys
assert!(!hybrid.ec_key().public_key_to_der().unwrap().is_empty());
assert!(!hybrid.mlkem_key().public_key_to_der().unwrap().is_empty());
assert!(!cloned.ec_key().public_key_to_der().unwrap().is_empty());
assert!(!cloned.mlkem_key().public_key_to_der().unwrap().is_empty());
// Verify the keys are equivalent
assert_eq!(
hybrid.ec_key().public_key_to_der().unwrap(),
cloned.ec_key().public_key_to_der().unwrap()
);
assert_eq!(
hybrid.mlkem_key().public_key_to_der().unwrap(),
cloned.mlkem_key().public_key_to_der().unwrap()
);
}
#[test]
fn test_hostkey_clone() {
let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid();
let hybrid = HybridPKey::new(
ec_key.public_key().unwrap(),
mlkem_key.public_key().unwrap(),
)
.unwrap();
// Test cloning V1
let v1_key = HostKey::V1(ec_key.public_key().unwrap());
let v1_cloned = v1_key.clone();
assert!(!v1_cloned.is_hybrid());
assert_eq!(
v1_key.ec_key().unwrap().public_key_to_der().unwrap(),
v1_cloned.ec_key().unwrap().public_key_to_der().unwrap()
);
// Test cloning V2
let v2_key = HostKey::V2(hybrid);
let v2_cloned = v2_key.clone();
assert!(v2_cloned.is_hybrid());
assert_eq!(
v2_key.ec_key().unwrap().public_key_to_der().unwrap(),
v2_cloned.ec_key().unwrap().public_key_to_der().unwrap()
);
assert_eq!(
v2_key.mlkem_key().unwrap().public_key_to_der().unwrap(),
v2_cloned.mlkem_key().unwrap().public_key_to_der().unwrap()
);
}
#[test]
fn test_hybrid_pkey_as_ref() {
let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid();
let hybrid = HybridPKey::new(
ec_key.public_key().unwrap(),
mlkem_key.public_key().unwrap(),
)
.unwrap();
let hybrid_ref: &HybridPKey = hybrid.as_ref();
assert!(!hybrid_ref.ec_key().public_key_to_der().unwrap().is_empty());
assert!(!hybrid_ref
.mlkem_key()
.public_key_to_der()
.unwrap()
.is_empty());
}
#[test]
fn test_hostkey_as_ref() {
let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid();
let hybrid = HybridPKey::new(
ec_key.public_key().unwrap(),
mlkem_key.public_key().unwrap(),
)
.unwrap();
let v1_key = HostKey::V1(ec_key.public_key().unwrap());
let v1_ref: &HostKey = v1_key.as_ref();
assert!(!v1_ref.is_hybrid());
let v2_key = HostKey::V2(hybrid);
let v2_ref: &HostKey = v2_key.as_ref();
assert!(v2_ref.is_hybrid());
}
}

View File

@@ -4,13 +4,11 @@
//! IBM Z Host key-slot implementations.
use openssl::hash::MessageDigest;
use openssl::hash::{DigestBytes, MessageDigest};
use openssl::pkey::{PKey, PKeyRef, Private, Public};
use super::ec_coord::EcPubKeyCoord;
use super::encrypt::Encrypt;
use crate::crypto::{derive_aes256_gcm_key, encrypt_aead, hash};
use crate::request::HostKey;
use crate::crypto::{derive_aes256_gcm_key, derive_aes256_gcm_key_hybrid, encrypt_aead, hash};
use crate::req::{EcPubKeyCoord, Encrypt, HostKey, HybridPKey};
use crate::Result;
/// IBM Z Host key-slot
@@ -71,12 +69,107 @@ impl Encrypt for KeyslotV1 {
}
}
/// IBM Z hybrid (V2) Host key-slot
///
/// Layout in binary format:
/// ```none
/// _______________________________________________________________
/// | Public Host Key Hash (64) |
/// | Wrapped(=Encrypted) Request Protection Key(32) |
/// | Key Slot Tag (16) |
/// | ML-KEM Ciphertext (1568) |
/// |_____________________________________________________________|
/// ```
#[derive(Debug, Clone)]
pub struct KeyslotV2 {
ec_hostkey: PKey<Public>,
mlkem_hostkey: PKey<Public>,
}
impl KeyslotV2 {
/// Size of a hybrid host-key hash
pub const PHKH_SIZE: u32 = 0x40;
/// Size of a complete V2 keyslot in bytes
pub const SIZE: usize = 1680;
/// Creates a new HybridKeyslot from the provided hybrid public key
pub fn new(hostkey: HybridPKey) -> Self {
let HybridPKey { ec_key, mlkem_key } = hostkey;
Self {
ec_hostkey: ec_key,
mlkem_hostkey: mlkem_key,
}
}
/// calculates the sha512 of this hybrid key
pub fn sha512(&self) -> Result<DigestBytes> {
let mut phk_buf = Vec::<u8>::with_capacity(160 + 1568);
let ec_phk: EcPubKeyCoord = self.ec_hostkey.as_ref().try_into()?;
phk_buf.extend_from_slice(ec_phk.as_ref());
phk_buf.extend_from_slice(&self.mlkem_hostkey.raw_public_key()?);
assert_eq!(phk_buf.len(), 160 + 1568);
let hash = hash(MessageDigest::sha512(), &phk_buf)?;
assert_eq!(hash.len(), 64);
Ok(hash)
}
/// Encrypts `secret` using `self` and `priv_key` the encryption.
///
/// # Returns
/// the encrypted data.
///
/// # Errors
///
/// This function will return an error if OpenSSL could not encrypt the secret.
pub fn encrypt(&self, secret: &[u8], priv_key: &PKeyRef<Private>) -> Result<Vec<u8>> {
let mut res = Vec::with_capacity(1680);
self.encrypt_to(secret, priv_key, &mut res)?;
Ok(res)
}
/// Encrypts the given request protection key `prot_key`.
///
/// The AES256 encryption key is derived from `self` as public key, and `priv_key` as private
/// key.
///
/// # Returns
/// The encrypted HybridKeyslot.
///
/// # Errors
///
/// This function will return an error if OpenSSL could not encrypt the secret.
fn encrypt_to(
&self,
prot_key: &[u8],
priv_key: &PKeyRef<Private>,
to: &mut Vec<u8>,
) -> Result<()> {
let (derived_key, ciphertext) =
derive_aes256_gcm_key_hybrid(priv_key, &self.ec_hostkey, &self.mlkem_hostkey)?;
let mut wrpk_and_kst =
encrypt_aead(&derived_key.into(), &[0; 12], &[], prot_key)?.into_buf();
assert_eq!(wrpk_and_kst.len(), 48);
to.reserve(1680);
let hash = self.sha512()?;
assert_eq!(hash.len(), 64);
to.extend_from_slice(&hash);
to.append(&mut wrpk_and_kst);
assert_eq!(ciphertext.len(), 1568);
to.extend_from_slice(&ciphertext);
Ok(())
}
}
/// Versioned keyslot container
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum Keyslot {
/// V1 key-slots with ECDH keys
V1(KeyslotV1),
/// V2 key-slots with hybrid ECDH/ML-KEM keys
V2(KeyslotV2),
}
impl Keyslot {
@@ -84,27 +177,34 @@ impl Keyslot {
pub fn new(hostkey: HostKey) -> Self {
match hostkey {
HostKey::V1(key) => Keyslot::V1(KeyslotV1::new(key)),
HostKey::V2(key) => Keyslot::V2(KeyslotV2::new(key)),
}
}
/// Return the public host key hash size for the given version of the key-slot in bytes
#[must_use]
pub fn phkh_size(&self) -> u32 {
match self {
Keyslot::V1(_) => KeyslotV1::PHKH_SIZE,
Keyslot::V2(_) => KeyslotV2::PHKH_SIZE,
}
}
/// Return the size of the key-slot in bytes
#[must_use]
pub fn size(&self) -> usize {
match self {
Keyslot::V1(_) => KeyslotV1::SIZE,
Keyslot::V2(_) => KeyslotV2::SIZE,
}
}
/// Return whether the key-slot uses hybrid keys
#[must_use]
pub fn is_hybrid(&self) -> bool {
match self {
Keyslot::V1(_) => false,
Keyslot::V2(_) => true,
}
}
}
@@ -118,6 +218,7 @@ impl Encrypt for Keyslot {
) -> Result<()> {
match self {
Keyslot::V1(ks) => ks.encrypt_to(secret, priv_key, to),
Keyslot::V2(ks) => ks.encrypt_to(secret, priv_key, to),
}
}
}
@@ -128,11 +229,17 @@ impl From<PKey<Public>> for Keyslot {
}
}
impl From<HybridPKey> for Keyslot {
fn from(key: HybridPKey) -> Self {
Keyslot::V2(KeyslotV2::new(key))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::get_test_asset;
use crate::test_utils::*;
use crate::test_utils::{DeterministicTestRandGuard, *};
#[test]
fn keyslot() {
@@ -147,4 +254,214 @@ mod tests {
let encr_ks = keyslot.encrypt(&[0x16u8; 32], &cust_key).unwrap();
assert_ne!(exp_keyslot, encr_ks);
}
#[test]
fn keyslot_v2() {
// Install deterministic RNG for reproducible encryption
let _guard = DeterministicTestRandGuard::install(&[0x42; 64], &[0x17; 16]).unwrap();
let (cust_key, host_key1, host_key2) = get_test_keys_hybrid();
let host_key = HostKey::V2(HybridPKey::new(host_key1, host_key2).unwrap());
let exp_keyslot = vec![
255, 94, 191, 53, 220, 196, 47, 37, 93, 227, 234, 101, 1, 174, 171, 68, 42, 136, 92,
238, 72, 6, 17, 77, 231, 225, 174, 22, 222, 188, 212, 15, 248, 145, 72, 126, 139, 17,
233, 225, 156, 46, 233, 151, 54, 2, 175, 88, 215, 254, 243, 222, 37, 81, 50, 110, 18,
76, 252, 12, 210, 146, 66, 23, 123, 246, 141, 14, 70, 157, 73, 124, 205, 112, 192, 82,
160, 243, 2, 154, 134, 145, 176, 147, 30, 217, 221, 93, 170, 239, 165, 37, 30, 192, 47,
94, 129, 165, 12, 137, 239, 12, 180, 199, 240, 160, 180, 205, 117, 84, 16, 148, 51, 48,
243, 216, 50, 132, 187, 115, 137, 3, 109, 145, 172, 159, 224, 25, 63, 96, 241, 105, 89,
35, 42, 180, 248, 35, 166, 81, 116, 175, 179, 252, 81, 188, 189, 42, 17, 60, 231, 132,
32, 111, 90, 138, 201, 16, 3, 97, 246, 130, 173, 85, 218, 187, 232, 92, 52, 120, 56,
142, 137, 101, 31, 21, 194, 186, 198, 38, 240, 58, 150, 93, 86, 18, 186, 5, 12, 41, 37,
94, 162, 119, 16, 237, 82, 252, 178, 159, 57, 58, 162, 20, 173, 140, 10, 172, 203, 241,
94, 110, 117, 32, 36, 217, 7, 168, 50, 105, 43, 33, 174, 140, 55, 88, 26, 233, 81, 253,
98, 121, 190, 70, 146, 84, 186, 6, 51, 181, 2, 59, 216, 45, 78, 4, 82, 90, 21, 180, 99,
239, 195, 254, 33, 171, 124, 97, 36, 110, 119, 142, 98, 125, 70, 236, 51, 192, 32, 8,
214, 224, 101, 169, 45, 173, 137, 194, 78, 114, 224, 76, 174, 188, 133, 227, 167, 169,
47, 241, 190, 15, 145, 217, 87, 254, 15, 150, 52, 138, 192, 230, 52, 129, 36, 8, 40,
37, 126, 103, 85, 86, 25, 78, 53, 243, 107, 193, 18, 102, 3, 83, 212, 5, 252, 224, 241,
7, 205, 220, 168, 183, 15, 37, 52, 190, 12, 9, 152, 124, 210, 65, 152, 210, 199, 81,
95, 240, 141, 196, 236, 8, 249, 221, 116, 130, 42, 52, 208, 218, 180, 27, 249, 59, 245,
21, 165, 142, 234, 140, 75, 89, 136, 250, 155, 157, 12, 154, 152, 188, 178, 227, 237,
124, 111, 176, 131, 227, 238, 164, 22, 88, 80, 217, 42, 217, 80, 148, 118, 12, 136, 95,
53, 69, 209, 131, 74, 91, 193, 253, 229, 146, 11, 58, 125, 19, 8, 229, 60, 177, 244,
74, 41, 92, 91, 62, 64, 221, 20, 213, 227, 4, 40, 22, 177, 155, 236, 147, 165, 27, 4,
99, 4, 151, 155, 82, 72, 151, 22, 102, 183, 192, 242, 18, 104, 146, 205, 107, 21, 254,
88, 23, 69, 246, 57, 217, 249, 124, 246, 54, 163, 244, 38, 74, 215, 144, 50, 45, 142,
36, 216, 88, 39, 70, 67, 88, 130, 1, 8, 205, 240, 159, 210, 205, 233, 237, 60, 81, 176,
112, 172, 187, 121, 239, 198, 43, 17, 49, 55, 170, 228, 243, 255, 76, 72, 121, 125,
157, 250, 93, 251, 55, 25, 4, 129, 67, 195, 30, 37, 6, 76, 10, 240, 178, 255, 151, 138,
36, 255, 32, 237, 133, 162, 130, 91, 9, 238, 67, 134, 4, 86, 225, 179, 166, 219, 4,
144, 7, 125, 248, 75, 132, 54, 56, 51, 237, 206, 252, 96, 9, 208, 140, 127, 143, 180,
32, 179, 254, 18, 233, 187, 122, 159, 172, 77, 204, 9, 179, 242, 153, 18, 138, 82, 13,
210, 140, 207, 9, 217, 216, 241, 106, 205, 109, 195, 223, 82, 172, 181, 48, 91, 124,
235, 85, 17, 18, 166, 216, 86, 120, 185, 54, 147, 156, 84, 106, 30, 235, 234, 114, 27,
23, 121, 210, 251, 136, 117, 53, 177, 28, 153, 144, 17, 70, 190, 206, 27, 135, 42, 227,
39, 243, 86, 170, 11, 63, 191, 102, 89, 92, 137, 103, 59, 142, 236, 203, 156, 231, 255,
30, 5, 86, 53, 59, 76, 181, 218, 184, 244, 21, 50, 100, 72, 81, 98, 193, 34, 58, 67,
150, 15, 17, 162, 216, 119, 213, 203, 50, 27, 158, 61, 105, 134, 151, 200, 68, 103, 74,
207, 12, 5, 163, 30, 198, 28, 41, 41, 97, 93, 251, 213, 133, 41, 225, 178, 67, 160, 85,
132, 146, 1, 201, 99, 49, 185, 27, 150, 213, 165, 134, 45, 248, 204, 67, 145, 49, 81,
35, 246, 25, 215, 209, 159, 106, 212, 14, 149, 193, 163, 90, 24, 83, 230, 178, 216,
194, 130, 118, 169, 81, 49, 49, 145, 96, 206, 216, 15, 134, 43, 130, 25, 110, 84, 39,
175, 223, 183, 209, 123, 5, 166, 244, 19, 89, 2, 47, 226, 3, 93, 156, 163, 67, 156,
237, 17, 59, 69, 99, 94, 32, 180, 64, 64, 115, 75, 44, 241, 150, 43, 169, 17, 36, 125,
216, 132, 92, 3, 244, 16, 83, 179, 192, 65, 133, 208, 19, 156, 17, 60, 54, 29, 253,
237, 18, 158, 145, 142, 232, 147, 2, 91, 21, 145, 125, 204, 243, 161, 245, 110, 140,
219, 206, 70, 235, 211, 167, 138, 104, 132, 248, 157, 65, 153, 216, 47, 125, 205, 237,
137, 220, 25, 228, 146, 194, 10, 169, 201, 224, 88, 119, 38, 140, 120, 125, 140, 46,
184, 221, 30, 10, 47, 34, 140, 173, 64, 38, 48, 77, 236, 206, 163, 111, 80, 46, 40,
232, 63, 247, 222, 25, 20, 246, 143, 9, 107, 172, 180, 84, 188, 234, 102, 87, 181, 173,
83, 14, 163, 170, 91, 29, 209, 93, 52, 158, 213, 6, 91, 71, 54, 244, 189, 198, 60, 21,
131, 210, 35, 18, 36, 164, 188, 87, 54, 73, 208, 115, 11, 248, 57, 107, 93, 23, 49,
129, 221, 61, 12, 172, 31, 199, 129, 196, 5, 184, 78, 226, 210, 83, 232, 153, 64, 17,
119, 243, 45, 73, 50, 129, 35, 94, 243, 146, 86, 136, 202, 86, 87, 97, 193, 59, 160,
181, 95, 150, 148, 117, 96, 31, 151, 97, 159, 53, 72, 171, 239, 210, 67, 207, 201, 109,
160, 230, 235, 176, 35, 235, 98, 128, 166, 195, 144, 200, 156, 7, 72, 242, 15, 95, 99,
69, 41, 219, 254, 244, 80, 158, 177, 64, 83, 11, 235, 98, 201, 130, 176, 119, 22, 214,
135, 215, 37, 65, 108, 91, 34, 94, 0, 153, 161, 87, 144, 177, 12, 122, 205, 11, 72,
233, 213, 63, 37, 12, 255, 235, 188, 194, 189, 65, 169, 185, 207, 131, 182, 243, 233,
30, 245, 39, 42, 188, 157, 28, 121, 231, 194, 121, 250, 121, 11, 28, 252, 98, 151, 238,
124, 56, 138, 106, 116, 187, 203, 21, 75, 239, 132, 19, 62, 43, 42, 12, 214, 87, 225,
217, 169, 134, 183, 193, 241, 175, 140, 36, 147, 248, 86, 6, 58, 212, 72, 112, 0, 56,
168, 128, 68, 253, 173, 225, 152, 167, 138, 254, 97, 123, 227, 163, 135, 198, 49, 49,
138, 249, 234, 245, 78, 150, 140, 170, 199, 41, 206, 246, 19, 117, 241, 27, 112, 102,
4, 94, 237, 30, 0, 68, 252, 163, 205, 10, 63, 146, 147, 37, 153, 197, 52, 162, 50, 250,
219, 130, 197, 3, 60, 246, 133, 83, 140, 103, 227, 50, 212, 121, 165, 114, 139, 225,
195, 145, 107, 249, 127, 194, 23, 112, 141, 242, 14, 218, 42, 131, 19, 245, 143, 73,
79, 194, 135, 224, 171, 249, 169, 129, 160, 153, 75, 66, 26, 12, 254, 180, 212, 229,
172, 134, 159, 122, 212, 219, 21, 29, 33, 11, 176, 149, 73, 169, 26, 150, 96, 133, 90,
217, 18, 37, 244, 48, 249, 4, 180, 129, 9, 45, 219, 106, 215, 28, 81, 118, 48, 98, 109,
167, 72, 107, 187, 78, 127, 251, 184, 170, 74, 57, 188, 91, 196, 229, 251, 70, 163, 68,
227, 238, 71, 115, 155, 246, 146, 97, 208, 21, 245, 62, 127, 47, 79, 131, 217, 41, 153,
52, 237, 159, 60, 98, 18, 169, 149, 6, 84, 135, 2, 45, 170, 165, 213, 201, 127, 23,
209, 2, 158, 235, 240, 195, 255, 76, 54, 189, 113, 50, 105, 230, 191, 217, 29, 46, 181,
80, 197, 60, 177, 243, 61, 52, 24, 140, 134, 147, 176, 198, 22, 92, 156, 217, 189, 134,
17, 122, 53, 49, 14, 87, 128, 99, 207, 123, 113, 169, 195, 206, 127, 211, 21, 216, 166,
18, 137, 110, 148, 70, 26, 54, 52, 113, 189, 45, 89, 254, 218, 247, 193, 71, 19, 153,
35, 179, 49, 237, 199, 176, 251, 143, 95, 115, 195, 3, 122, 161, 243, 220, 39, 102,
147, 134, 25, 172, 164, 167, 110, 123, 221, 177, 28, 236, 100, 165, 186, 179, 45, 189,
183, 76, 171, 127, 209, 108, 220, 83, 207, 136, 98, 54, 76, 37, 188, 57, 157, 204, 109,
150, 181, 110, 57, 5, 26, 168, 34, 11, 117, 3, 184, 147, 155, 122, 244, 251, 215, 1,
211, 226, 185, 214, 120, 206, 212, 75, 203, 174, 140, 20, 93, 93, 207, 28, 15, 122, 9,
83, 98, 107, 51, 202, 151, 220, 42, 95, 17, 141, 141, 201, 149, 253, 55, 169, 170, 237,
166, 92, 92, 20, 89, 124, 167, 102, 161, 87, 97, 88, 20, 245, 175, 32, 111, 0, 2, 192,
25, 63, 182, 10, 226, 165, 162, 223, 35, 243, 198, 189, 167, 137, 134, 207, 84, 240, 8,
95, 137, 31, 159, 58, 211, 161, 94, 150, 54, 188, 145, 253, 206, 156, 130, 121, 192,
221, 73, 249, 92, 91, 184, 211, 131, 206, 205, 183, 159, 195, 170, 47, 13, 131, 6, 132,
103, 121, 40, 252, 250, 251, 85, 253, 68, 66, 211, 24, 104, 48, 150, 176, 62, 201, 161,
93, 204, 120, 196, 159, 208, 199, 96, 228, 239, 29, 239, 128, 156, 14, 131, 25, 27,
157, 249, 253, 250, 193, 148, 133, 59, 138, 202, 239,
];
let keyslot = Keyslot::new(host_key);
let encr_keyslot = keyslot.encrypt(&[0x17u8; 32], &cust_key).unwrap();
assert_eq!(encr_keyslot, exp_keyslot);
}
#[test]
fn test_keyslot_v2_constants() {
// Test V2 keyslot constants
assert_eq!(
KeyslotV2::SIZE,
1680,
"V2 keyslot size should be 1680 bytes"
);
assert_eq!(
KeyslotV2::PHKH_SIZE,
0x40,
"V2 PHKH size should be 64 bytes (SHA-512)"
);
}
#[test]
fn test_keyslot_v2_creation() {
let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid();
let hybrid = HybridPKey::new(
ec_key.public_key().unwrap(),
mlkem_key.public_key().unwrap(),
)
.unwrap();
let keyslot = KeyslotV2::new(hybrid);
// Verify keyslot was created successfully
assert!(keyslot.ec_hostkey.public_key_to_der().is_ok());
assert!(keyslot.mlkem_hostkey.public_key_to_der().is_ok());
}
#[test]
fn test_keyslot_enum_v1_variant() {
let (_, host_key) = get_test_keys();
let keyslot = Keyslot::V1(KeyslotV1(host_key));
assert_eq!(keyslot.phkh_size(), KeyslotV1::PHKH_SIZE);
assert_eq!(keyslot.size(), KeyslotV1::SIZE);
assert!(matches!(keyslot, Keyslot::V1(_)));
}
#[test]
fn test_keyslot_enum_v2_variant() {
let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid();
let hybrid = HybridPKey::new(
ec_key.public_key().unwrap(),
mlkem_key.public_key().unwrap(),
)
.unwrap();
let keyslot = Keyslot::V2(KeyslotV2::new(hybrid));
assert_eq!(keyslot.phkh_size(), KeyslotV2::PHKH_SIZE);
assert_eq!(keyslot.size(), KeyslotV2::SIZE);
assert!(matches!(keyslot, Keyslot::V2(_)));
}
#[test]
fn test_keyslot_from_hostkey_v1() {
let (_, ec_key) = get_test_keys();
let hostkey = HostKey::V1(ec_key);
let keyslot = Keyslot::new(hostkey);
assert!(matches!(keyslot, Keyslot::V1(_)));
assert_eq!(keyslot.size(), KeyslotV1::SIZE);
}
#[test]
fn test_keyslot_from_hostkey_v2() {
let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid();
let hybrid = HybridPKey::new(
ec_key.public_key().unwrap(),
mlkem_key.public_key().unwrap(),
)
.unwrap();
let hostkey = HostKey::V2(hybrid);
let keyslot = Keyslot::new(hostkey);
assert!(matches!(keyslot, Keyslot::V2(_)));
assert_eq!(keyslot.size(), KeyslotV2::SIZE);
}
#[test]
fn test_keyslot_from_hybrid_public_key() {
let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid();
let hybrid = HybridPKey::new(
ec_key.public_key().unwrap(),
mlkem_key.public_key().unwrap(),
)
.unwrap();
let keyslot: Keyslot = hybrid.into();
assert!(matches!(keyslot, Keyslot::V2(_)));
}
#[test]
fn test_keyslot_sizes() {
// Document the sizes for V1 and V2
assert_eq!(KeyslotV1::SIZE, 80, "V1 keyslot size");
assert_eq!(
KeyslotV2::SIZE,
1680,
"V2 keyslot size (includes ML-KEM ciphertext)"
);
}
}

View File

@@ -17,9 +17,10 @@ mod request;
// Re-export public types
pub use context::ReqEncrCtx;
pub(crate) use ec_coord::get_pub_ecdh_points;
pub use ec_coord::EcPubKeyCoord;
pub use encrypt::{Aad, Encrypt};
pub use header::RequestHdr;
pub use hostkey::HostKey;
pub use keyslot::{Keyslot, KeyslotV1};
pub use hostkey::{HostKey, HybridPKey};
pub use keyslot::{Keyslot, KeyslotV1, KeyslotV2};
pub use request::{BinReqValues, Request};

View File

@@ -51,7 +51,7 @@ pub trait Request {
/// Add a host-key to this request
///
/// Must be called at least once, otherwise {`Request::encrypt`} will fail
fn add_hostkey(&mut self, hostkey: HostKey);
fn add_hostkey(&mut self, hostkey: HostKey) -> Result<()>;
}
/// A struct to represent some parts of a binary/encrypted request.
@@ -148,10 +148,9 @@ mod tests {
use zerocopy::IntoBytes;
use super::*;
use crate::crypto::SymKey;
use crate::get_test_asset;
use crate::req::header::RequestHdr;
use crate::req::{Aad, HostKey, Keyslot, ReqEncrCtx};
use crate::request::SymKey;
use crate::req::{Aad, HybridPKey, Keyslot};
use crate::test_utils::*;
static TEST_MAGIC: [u8; 8] = 0x12345689abcdef00u64.to_be_bytes();
@@ -187,6 +186,37 @@ mod tests {
assert_eq!(&aad, &aad_exp);
}
#[test]
fn encr_build_aad_v2() {
let (cust_key, host_key1, host_key2) = get_test_keys_hybrid();
let ks = Keyslot::new(HostKey::V2(HybridPKey::new(host_key1, host_key2).unwrap()));
let ctx = ReqEncrCtx::new_aes_256(
Some([0x11; 12]),
Some(cust_key),
Some(SymKey::Aes256([0x17; 32].into())),
)
.unwrap();
let v = [0x55; 8];
let aad = Aad::Plain(&v);
let aad = ctx
.build_aad(0x200, &vec![aad, Aad::Ks(&ks)], 16, Some(TEST_MAGIC))
.unwrap();
let aad_exp = vec![
0x12, 0x34, 0x56, 0x89, 0xab, 0xcd, 0xef, 0, // progr
0, 0, 2, 0, // vers
0, 0, 6, 232, // size
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, // iv
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // res
1, // nks
0, 0, 0, 0, // res
0, 0, 0, 16, // sea
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, // aad
];
// only compare non-randomized part
assert_eq!(aad[..aad_exp.len()], aad_exp);
}
#[test]
fn encr_build_aad_nks_no() {
let ctx = ReqEncrCtx::new_aes_256(Some([0x11; 12]), None, None).unwrap();
@@ -210,6 +240,21 @@ mod tests {
let aad = ctx.build_aad(0x200, &aad, 16, Some(TEST_MAGIC));
assert!(matches!(aad, Err(Error::ManyHostkeys)));
}
#[test]
fn encr_build_aad_nks_many_v2() {
let (_, host_key1, host_key2) = get_test_keys_hybrid();
let host_key = HostKey::V2(HybridPKey::new(host_key1, host_key2).unwrap());
let ctx = ReqEncrCtx::new_aes_256(Some([0x11; 12]), None, None).unwrap();
let ks: Vec<Keyslot> = (0..257).map(|_| Keyslot::new(host_key.clone())).collect();
let mut aad = Vec::<Aad>::new();
ks.iter().for_each(|ks| aad.push(Aad::Ks(ks)));
let aad = ctx.build_aad(0x200, &aad, 16, Some(TEST_MAGIC));
assert!(matches!(aad, Err(Error::ManyHostkeys)));
}
#[test]
fn encr_build_aad_nks() {
let (_, host_key) = get_test_keys();
@@ -229,6 +274,25 @@ mod tests {
assert_eq!(aad.get(39).unwrap(), &3u8);
}
#[test]
fn encr_build_aad_nks_v2() {
let (_, host_key1, host_key2) = get_test_keys_hybrid();
let host_key = HostKey::V2(HybridPKey::new(host_key1, host_key2).unwrap());
let ctx = ReqEncrCtx::new_aes_256(Some([0x11; 12]), None, None).unwrap();
let ks = [
Keyslot::new(host_key.clone()),
Keyslot::new(host_key.clone()),
Keyslot::new(host_key),
];
let mut aad = Vec::<Aad>::new();
ks.iter().for_each(|ks| aad.push(Aad::Ks(ks)));
let aad = ctx.build_aad(0x200, &aad, 16, Some(TEST_MAGIC)).unwrap();
assert_eq!(aad.get(39).unwrap(), &3u8);
}
#[test]
fn req_hdr() {
let hdr = RequestHdr::new(0x200, 22, [0x11; 12], 15, 44, None);

View File

@@ -89,6 +89,27 @@ pub fn get_test_key_and_cert() -> (PKey<Private>, X509) {
(cust_key, host_key)
}
pub fn get_test_key_and_cert_hybrid() -> (PKey<Private>, X509, X509) {
let pub_key = get_test_asset!("keys/public_cust.bin");
let priv_key = get_test_asset!("keys/private_cust.bin");
let host_key = get_test_asset!("keys/host.ec.crt");
let host_keys = get_test_asset!("keys/host.hybrid.crt");
assert_eq!(pub_key.len(), 160);
assert_eq!(priv_key.len(), 80);
let cust_key = get_keypair(pub_key, priv_key).unwrap();
let host_key1 = X509::from_pem(host_key).unwrap();
let host_keys = X509::stack_from_pem(host_keys).unwrap();
assert_eq!(host_keys.len(), 2);
println!("host_key1 = {host_key1:?}");
println!("host_keys[0] = {:?}", host_keys[0]);
println!("host_keys[1] = {:?}", host_keys[1]);
(cust_key, host_key1, host_keys[1].clone())
}
/// TEST ONLY! Get a fixed private/public pair and a fixed public key
///
/// Intended for TESTING only. All parts of the key including the private key are checked in git and
@@ -98,6 +119,15 @@ pub fn get_test_keys() -> (PKey<Private>, PKey<Public>) {
(cust_key, host.public_key().unwrap())
}
pub fn get_test_keys_hybrid() -> (PKey<Private>, PKey<Public>, PKey<Public>) {
let (cust_key, host_key_1, host_key_2) = get_test_key_and_cert_hybrid();
(
cust_key,
host_key_1.public_key().unwrap(),
host_key_2.public_key().unwrap(),
)
}
fn read_ecdh_pubkey(coords: &[u8]) -> Result<PKey<Public>, ErrorStack> {
assert!(coords.len() == 160);
let x = BigNum::from_slice(&coords[..80])?;

View File

@@ -4,6 +4,7 @@
use std::mem::size_of;
use pv_core::static_assert;
use zerocopy::{BigEndian, FromBytes, Immutable, IntoBytes, KnownLayout, U32};
use super::additional::{FW_STATE_SIZE, PHKH_SIZE, SECRET_STORE_HASH_SIZE};
@@ -14,7 +15,7 @@ use crate::misc::Flags;
use crate::req::{Aad, BinReqValues, HostKey, Keyslot, ReqEncrCtx};
use crate::request::{Confidential, MagicValue, Request, RequestVersion, SymKey, Zeroize};
use crate::uv::UvFlags;
use crate::{assert_size, static_assert, Error, Result};
use crate::{assert_size, Error, Result};
#[cfg(doc)]
use crate::{
request::SymKeyType,
@@ -60,9 +61,9 @@ use crate::{
/// let hkd = s390_pv::misc::read_certs(&std::fs::read("host-key-document.crt")?)?;
/// // IBM issued HKD certificates typically have one X509
/// let hkd = hkd.first().unwrap().public_key()?;
/// arcb.add_hostkey(HostKey::V1(hkd));
/// arcb.add_hostkey(HostKey::V1(hkd))?;
/// // you can add multiple hostkeys
/// // arcb.add_hostkey(another_hkd);
/// // arcb.add_hostkey(HostKey::V1(another_hkd))?;
/// // encrypt it
/// let ctx = ReqEncrCtx::random(SymKeyType::Aes256Gcm)?;
/// let arcb = arcb.encrypt(&ctx)?;
@@ -106,7 +107,13 @@ impl AttestationRequest {
/// Returns a reference to the flags of this [`AttestationRequest`].
pub fn flags(&self) -> &AttestationFlags {
&self.aad.flags
self.aad.flags()
}
/// Returns the request version, derived from the type of added host-keys.
/// Returns [`AttestationVersion::One`] if no host-keys have been added yet.
pub fn version(&self) -> AttestationVersion {
self.version
}
/// Returns a copy of the confidential data of this [`AttestationRequest`].
@@ -142,6 +149,7 @@ impl AttestationRequest {
let values = BinReqValues::get(arcb)?;
match values.version().try_into()? {
AttestationVersion::One => (),
AttestationVersion::Two => (),
};
Ok(values)
@@ -235,8 +243,19 @@ impl Request for AttestationRequest {
ctx.encrypt_aead(&aad, conf).map(|res| res.into_buf())
}
fn add_hostkey(&mut self, hostkey: HostKey) {
self.keyslots.push(Keyslot::new(hostkey))
fn add_hostkey(&mut self, hostkey: HostKey) -> Result<()> {
match self.version {
AttestationVersion::One if !hostkey.is_hybrid() => Ok(()),
AttestationVersion::Two if hostkey.is_hybrid() => Ok(()),
AttestationVersion::One => Err(Error::InvalidHkd(
"Add classical hostkey to a v1 attestation request".to_string(),
)),
AttestationVersion::Two => Err(Error::InvalidHkd(
"Add hybrid key to a v2 attetstation request".to_string(),
)),
}?;
self.keyslots.push(Keyslot::new(hostkey));
Ok(())
}
}
@@ -246,6 +265,8 @@ impl Request for AttestationRequest {
pub enum AttestationVersion {
/// Version 1 (= 0x0100)
One = 0x0100,
/// Version 2 (= 0x0200)
Two = 0x0200,
}
impl TryFrom<u32> for AttestationVersion {
@@ -254,6 +275,8 @@ impl TryFrom<u32> for AttestationVersion {
fn try_from(value: u32) -> Result<Self> {
if value == Self::One as u32 {
Ok(Self::One)
} else if value == Self::Two as u32 {
Ok(Self::Two)
} else {
Err(Error::BinArcbInvVersion(value))
}
@@ -412,8 +435,8 @@ impl Zeroize for ReqConfData {
mod test {
use super::*;
use crate::get_test_asset;
use crate::request::SymKey;
use crate::test_utils::get_test_keys;
use crate::request::{HybridPKey, SymKey};
use crate::test_utils::{get_test_keys, get_test_keys_hybrid};
const ARPK: [u8; 32] = [0x17; 32];
const NONCE: [u8; 16] = [0xab; 16];
@@ -421,6 +444,8 @@ mod test {
fn mk_arcb() -> Vec<u8> {
let (cust_key, host_key) = get_test_keys();
let host_key = HostKey::V1(host_key);
let ctx = ReqEncrCtx::new_aes_256(
Some([0x55; 12]),
Some(cust_key),
@@ -443,7 +468,37 @@ mod test {
arcb.conf.value_mut().nonce = NONCE;
arcb.conf.value_mut().meas_key = MEAS;
arcb.add_hostkey(HostKey::V1(host_key));
arcb.add_hostkey(host_key).unwrap();
arcb.encrypt(&ctx).unwrap()
}
fn mk_arcb_v2() -> Vec<u8> {
let (cust_key, host_key1, host_key2) = get_test_keys_hybrid();
let host_key = HostKey::V2(HybridPKey::new(host_key1, host_key2).unwrap());
let ctx = ReqEncrCtx::new_aes_256(
Some([0x55; 12]),
Some(cust_key),
Some(SymKey::Aes256(ARPK.into())),
)
.unwrap();
let mut flags = AttestationFlags::default();
flags.set_image_phkh();
flags.set_attest_phkh();
let mut arcb = AttestationRequest::new(
AttestationVersion::Two,
AttestationMeasAlg::HmacSha512,
flags,
)
.unwrap();
// manually set confidential data (API does not allow this)
arcb.conf.value_mut().nonce = NONCE;
arcb.conf.value_mut().meas_key = MEAS;
arcb.add_hostkey(host_key).unwrap();
arcb.encrypt(&ctx).unwrap()
}
@@ -455,6 +510,32 @@ mod test {
assert_eq!(request, exp);
}
#[test]
fn arcb_v2() {
let request = mk_arcb_v2();
// Expected bytes for a V2 ARCB: rqvn = 0x0200 (bytes 8-11), rest deterministic.
// The first 288 bytes cover header + customer-public-key + one V2 keyslot header.
let exp: [u8; 288] = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 7, 208, 85, 85, 85, 85, 85, 85, 85, 85, 85,
85, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 80, 112, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 199,
93, 52, 249, 22, 82, 219, 69, 123, 11, 32, 156, 70, 164, 145, 164, 78, 226, 177, 110,
35, 194, 216, 218, 241, 22, 103, 138, 98, 242, 76, 227, 50, 197, 153, 95, 8, 69, 107,
102, 177, 109, 213, 90, 146, 197, 7, 241, 227, 26, 247, 140, 100, 168, 46, 122, 84, 27,
21, 19, 80, 21, 242, 2, 134, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 64, 128, 88,
167, 241, 165, 195, 80, 151, 83, 58, 2, 169, 56, 121, 231, 222, 103, 186, 40, 11, 206,
131, 101, 236, 148, 178, 185, 8, 245, 137, 195, 169, 152, 216, 190, 30, 99, 7, 215, 74,
224, 26, 220, 70, 130, 95, 246, 187, 111, 160, 92, 17, 71, 207, 226, 204, 244, 162, 79,
61, 131, 61, 218, 112, 255, 94, 191, 53, 220, 196, 47, 37, 93, 227, 234, 101, 1, 174,
171, 68, 42, 136, 92, 238, 72, 6, 17, 77, 231, 225, 174, 22, 222, 188, 212, 15, 248,
145, 72, 126, 139, 17, 233, 225, 156, 46, 233, 151, 54, 2, 175, 88, 215, 254, 243, 222,
37, 81, 50, 110, 18, 76, 252, 12, 210, 146, 66, 23,
];
// only compare non-randomized part
assert_eq!(request[..288], exp[..288]);
}
#[test]
fn auth_bin() {
let request = mk_arcb();
@@ -464,6 +545,15 @@ mod test {
assert_eq!(exp, auth_bin.as_bytes());
}
#[test]
fn auth_bin_v2() {
let request = mk_arcb_v2();
let auth_bin = AttestationRequest::auth_bin(&request).unwrap();
let exp = &request[0x30..0x40];
assert_eq!(exp, auth_bin.as_bytes());
}
#[test]
fn decrypt_bin() {
let request = mk_arcb();
@@ -473,6 +563,100 @@ mod test {
assert_eq!(conf.nonce().as_ref().unwrap().value(), &NONCE);
}
#[test]
fn decrypt_bin_v2() {
let request = mk_arcb_v2();
let arpk = SymKey::Aes256(ARPK.into());
let (_, conf) = AttestationRequest::decrypt_bin(&request, &arpk).unwrap();
assert_eq!(conf.measurement_key(), &MEAS);
assert_eq!(conf.nonce().as_ref().unwrap().value(), &NONCE);
}
#[test]
fn arcb_v1_version() {
// Without any host-keys, version defaults to One
let arcb = AttestationRequest::new(
AttestationVersion::One,
AttestationMeasAlg::HmacSha512,
AttestationFlags::default(),
)
.unwrap();
assert_eq!(arcb.version(), AttestationVersion::One);
}
#[test]
fn arcb_v2_version() {
// After adding a V2 host-key, version is Two
let mut arcb = AttestationRequest::new(
AttestationVersion::Two,
AttestationMeasAlg::HmacSha512,
AttestationFlags::default(),
)
.unwrap();
let (_, host_key1, host_key2) = get_test_keys_hybrid();
arcb.add_hostkey(HostKey::V2(HybridPKey::new(host_key1, host_key2).unwrap()))
.unwrap();
assert_eq!(arcb.version(), AttestationVersion::Two);
}
#[test]
fn attestation_version_try_from() {
// Test version conversion from u32
assert_eq!(
AttestationVersion::try_from(0x0100).unwrap(),
AttestationVersion::One
);
assert_eq!(
AttestationVersion::try_from(0x0200).unwrap(),
AttestationVersion::Two
);
// Invalid version should error
assert!(AttestationVersion::try_from(0x0300).is_err());
}
#[test]
fn attestation_flags_expected_size() {
// Test expected additional data size calculation for V1
let mut flags = AttestationFlags::default();
// Image PHKH flag - should be 32 bytes
flags.set_image_phkh();
assert_eq!(flags.expected_additional_size(), 32);
// Add attest PHKH flag - should be 64 bytes (32 + 32)
flags.set_attest_phkh();
assert_eq!(flags.expected_additional_size(), 64);
// Add secret store hash - should be 128 bytes (64 + 64)
flags.set_secret_store_hash();
assert_eq!(flags.expected_additional_size(), 128);
// Add firmware state - should be 448 bytes (128 + 320)
flags.set_firmware_state();
assert_eq!(flags.expected_additional_size(), 448);
}
#[test]
fn confidential_data_v2() {
// Test confidential data extraction (version-independent)
let arcb = AttestationRequest::new(
AttestationVersion::Two,
AttestationMeasAlg::HmacSha512,
AttestationFlags::default(),
)
.unwrap();
let conf = arcb.confidential_data();
// Should have measurement key and nonce
assert_eq!(conf.measurement_key().len(), 64);
assert!(conf.nonce().is_some());
assert_eq!(conf.nonce().as_ref().unwrap().value().len(), 16);
}
#[test]
fn decrypt_bin_fail_magic() {
let arpk = SymKey::Aes256(ARPK.into());
@@ -484,6 +668,17 @@ mod test {
assert!(matches!(ret, Err(Error::NoArcb)));
}
#[test]
fn decrypt_bin_fail_magic_v2() {
let arpk = SymKey::Aes256(ARPK.into());
let mut tamp_arcb = mk_arcb_v2();
// tamper magic
tamp_arcb[0] = 17;
let ret = AttestationRequest::decrypt_bin(&tamp_arcb, &arpk);
assert!(matches!(ret, Err(Error::NoArcb)));
}
#[test]
fn decrypt_bin_fail_mai() {
let arpk = SymKey::Aes256(ARPK.into());
@@ -499,6 +694,21 @@ mod test {
));
}
#[test]
fn decrypt_bin_fail_mai_v2() {
let arpk = SymKey::Aes256(ARPK.into());
let mut tamp_arcb = mk_arcb_v2();
// tamper MAI
tamp_arcb[0x3b] = 17;
let ret = AttestationRequest::decrypt_bin(&tamp_arcb, &arpk);
println!("{ret:?}");
assert!(matches!(
ret,
Err(Error::PvCore(pv_core::Error::BinArcbInvAlgorithm(17)))
));
}
#[test]
fn decrypt_bin_fail_aad() {
let arpk = SymKey::Aes256(ARPK.into());
@@ -509,4 +719,15 @@ mod test {
let ret = AttestationRequest::decrypt_bin(&tamp_arcb, &arpk);
assert!(matches!(ret, Err(Error::GcmTagMismatch)));
}
#[test]
fn decrypt_bin_fail_aad_v2() {
let arpk = SymKey::Aes256(ARPK.into());
let mut tamp_arcb = mk_arcb_v2();
// tamper AAD
tamp_arcb[0x3c] = 17;
let ret = AttestationRequest::decrypt_bin(&tamp_arcb, &arpk);
assert!(matches!(ret, Err(Error::GcmTagMismatch)));
}
}

View File

@@ -6,6 +6,7 @@ use std::mem::size_of;
use openssl::hash::MessageDigest;
use openssl::pkey::{PKeyRef, Private};
use pv_core::misc::write_file;
use zerocopy::{BigEndian, IntoBytes, U16, U32};
use super::AttNonce;
@@ -101,6 +102,7 @@ impl AttestationItems {
}
items.extend_from_slice(additional);
assert!(items.len() == size);
write_file("additional_data_parsed", &items, "blah").unwrap();
Self(items.into())
}
}

View File

@@ -6,31 +6,32 @@ use openssl::md::Md;
use openssl::pkey::{PKey, Private};
use pv_core::request::RequestVersion;
use pv_core::secret::AddSecretMagic;
use pv_core::static_assert;
use pv_core::uv::SecretId;
use zerocopy::{Immutable, IntoBytes};
use super::guest_secret::ListableSecretHdr;
use super::guest_secret::{ListableSecretHdr, SecretAuth};
use super::user_data::UserData;
use crate::crypto::{hkdf_rfc_5869, AeadEncryptionResult};
use crate::misc::Flags;
use crate::req::{Aad, BinReqValues, HostKey, Keyslot, ReqEncrCtx};
use crate::req::{Aad, BinReqValues, HostKey, Keyslot, ReqEncrCtx, RequestHdr};
use crate::request::{BootHdrTags, Confidential, Request};
use crate::secret::{ExtSecret, GuestSecret};
use crate::uv::{ConfigUid, UvFlags};
use crate::{assert_size, Result};
use crate::{assert_size, Error, Result};
/// Authenticated data w/o user data
#[repr(C)]
#[derive(Debug, Clone, Copy, IntoBytes, Immutable)]
struct ReqAuthData {
struct ReqAuthDataV1 {
flags: UvFlags,
boot_tags: BootHdrTags,
cuid: ConfigUid,
reserved90: [u8; 0x100],
}
assert_size!(ReqAuthData, 0x1e8);
assert_size!(ReqAuthDataV1, 0x1e8);
impl ReqAuthData {
impl ReqAuthDataV1 {
fn new<F: Into<UvFlags>>(boot_tags: BootHdrTags, flags: F) -> Self {
Self {
flags: flags.into(),
@@ -41,6 +42,30 @@ impl ReqAuthData {
}
}
/// Authenticated data w/o user data for v2 header: move up secret header 2
#[repr(C)]
#[derive(Debug, Clone, IntoBytes, Immutable)]
struct ReqAuthDataV2 {
flags: UvFlags,
boot_tags: BootHdrTags,
cuid: ConfigUid,
secr_auth: [u8; 0x30],
reservedd0: [u8; 0x100 - 0x30],
}
assert_size!(ReqAuthDataV2, 0x1e8);
impl ReqAuthDataV2 {
fn new<F: Into<UvFlags>>(boot_tags: BootHdrTags, flags: F, secr_auth: [u8; 0x30]) -> Self {
Self {
flags: flags.into(),
boot_tags,
cuid: [0; 0x10],
secr_auth,
reservedd0: [0; 0xd0],
}
}
}
#[derive(Debug)]
struct ReqConfData {
secret: GuestSecret,
@@ -92,6 +117,8 @@ impl From<AddSecretFlags> for UvFlags {
pub enum AddSecretVersion {
/// Version 1 (= 0x0100)
One = 0x0100,
/// Version 2 (= 0x0200)
Two = 0x0200,
#[cfg(not(doc))]
#[cfg(any(debug_assertions, test))]
@@ -99,17 +126,182 @@ pub enum AddSecretVersion {
Inv = 0,
}
impl TryFrom<u32> for AddSecretVersion {
type Error = Error;
fn try_from(value: u32) -> Result<Self> {
if value == Self::One as u32 {
Ok(Self::One)
} else if value == Self::Two as u32 {
Ok(Self::Two)
} else {
Err(Error::BinAsrcbInvVersion(value))
}
}
}
impl From<AddSecretVersion> for RequestVersion {
fn from(val: AddSecretVersion) -> Self {
val as Self
}
}
impl From<crate::request::SeHdrVersion> for AddSecretVersion {
fn from(val: crate::request::SeHdrVersion) -> Self {
match val {
crate::request::SeHdrVersion::One => AddSecretVersion::One,
crate::request::SeHdrVersion::Two => AddSecretVersion::Two,
}
}
}
/// Trait for authenticated data in add-secret requests.
///
/// This trait provides a common interface for different versions of authenticated data,
/// allowing flexible addition of new versions in the future.
trait ReqAuthData: IntoBytes + Immutable {
/// Get the configuration UID
fn cuid(&self) -> &ConfigUid;
/// Set the configuration UID
fn set_cuid(&mut self, cuid: ConfigUid);
/// Get the boot tags
fn boot_tags(&self) -> &BootHdrTags;
/// Get the flags
fn flags(&self) -> &UvFlags;
}
impl ReqAuthData for ReqAuthDataV1 {
fn cuid(&self) -> &ConfigUid {
&self.cuid
}
fn set_cuid(&mut self, cuid: ConfigUid) {
self.cuid = cuid;
}
fn boot_tags(&self) -> &BootHdrTags {
&self.boot_tags
}
fn flags(&self) -> &UvFlags {
&self.flags
}
}
impl ReqAuthData for ReqAuthDataV2 {
fn cuid(&self) -> &ConfigUid {
&self.cuid
}
fn set_cuid(&mut self, cuid: ConfigUid) {
self.cuid = cuid;
}
fn boot_tags(&self) -> &BootHdrTags {
&self.boot_tags
}
fn flags(&self) -> &UvFlags {
&self.flags
}
}
/// Enum holding version-specific authenticated data
#[derive(Debug)]
enum ReqAuthDataVersion {
V1(ReqAuthDataV1),
V2(ReqAuthDataV2),
}
impl ReqAuthDataVersion {
fn new(
version: AddSecretVersion,
boot_tags: BootHdrTags,
flags: AddSecretFlags,
conf_data: &SecretAuth,
) -> Result<Self> {
Ok(match version {
AddSecretVersion::One => Self::V1(ReqAuthDataV1::new(boot_tags, flags)),
AddSecretVersion::Two => Self::V2(ReqAuthDataV2::new(
boot_tags,
flags,
conf_data.get(version).try_into().expect(
"SecretAuth::get() must return exactly 0x30 bytes for AddSecretVersion::Two",
),
)),
#[cfg(any(debug_assertions, test))]
AddSecretVersion::Inv => panic!("Invalid version for production use"),
})
}
#[allow(dead_code)]
fn cuid(&self) -> &ConfigUid {
match self {
Self::V1(v) => v.cuid(),
Self::V2(v) => v.cuid(),
}
}
fn set_cuid(&mut self, cuid: ConfigUid) {
match self {
Self::V1(v) => v.set_cuid(cuid),
Self::V2(v) => v.set_cuid(cuid),
}
}
fn boot_tags(&self) -> &BootHdrTags {
match self {
Self::V1(v) => v.boot_tags(),
Self::V2(v) => v.boot_tags(),
}
}
#[allow(dead_code)]
fn flags(&self) -> &UvFlags {
match self {
Self::V1(v) => v.flags(),
Self::V2(v) => v.flags(),
}
}
fn as_bytes(&self) -> &[u8] {
match self {
Self::V1(v) => v.as_bytes(),
Self::V2(v) => v.as_bytes(),
}
}
}
/// Add-secret request Control Block
///
/// An ASRCB wraps a secret to securely transport it to the Ultravisor.
///
/// Layout:
/// Layout V2:
/// ```none
/// _______________________________________________________________
/// | generic header (48)
/// | --------------------------------------------------- |
/// | Plaintext Add-Secret flags (8) |
/// | SE header tags: PLD(64) ALD(64) TLD(64) HeaderTag(16) |
/// | Configuration unique ID(16) (Attestation) |
/// | Optional, defaults to 0 |
/// | Secret header (48) |
/// | Reserved(208) |
/// | User Data(512) (reserved) |
/// | Customer Public Key (160) generated for each request |
/// | N Keyslots(1680 each) |
/// | --------------------------------------------------- |
/// | Secret to add (Secret type dependent)(may be 0 bytes) | Encrypted
/// | Extension secret(32) Optional, defaults to 0 | Encrypted
/// | --------------------------------------------------- |
/// | AES GCM Tag (16) |
/// |_____________________________________________________________|
/// ```
///
/// Layout V1:
/// ```none
/// _______________________________________________________________
/// | generic header (48)
@@ -133,15 +325,19 @@ impl From<AddSecretVersion> for RequestVersion {
#[derive(Debug)]
pub struct AddSecretRequest {
version: AddSecretVersion,
aad: ReqAuthData,
aad: ReqAuthDataVersion,
keyslots: Vec<Keyslot>,
conf: ReqConfData,
user_data: UserData,
}
static_assert!(AddSecretRequest::USER_DATA_OFFS == 0x218);
static_assert!(
AddSecretRequest::USER_DATA_OFFS == size_of::<RequestHdr>() + size_of::<ReqAuthDataV2>()
);
impl AddSecretRequest {
/// Offset of the user-data in the add-secret request in bytes
pub(super) const V1_USER_DATA_OFFS: usize = 0x218;
pub(super) const USER_DATA_OFFS: usize = size_of::<RequestHdr>() + size_of::<ReqAuthDataV1>();
/// Create a new add-secret request.
///
@@ -152,22 +348,23 @@ impl AddSecretRequest {
secret: GuestSecret,
boot_tags: BootHdrTags,
flags: AddSecretFlags,
) -> Self {
Self {
conf: ReqConfData {
extension_secret: Confidential::new([0; 32]),
secret,
},
aad: ReqAuthData::new(boot_tags, flags),
) -> Result<Self> {
let conf = ReqConfData {
extension_secret: Confidential::new([0; 32]),
secret,
};
Ok(Self {
aad: ReqAuthDataVersion::new(version, boot_tags, flags, &conf.secret.auth())?,
conf,
keyslots: vec![],
version,
user_data: UserData::Null,
}
})
}
/// Sets the Configuration Unique Id of this [`AddSecretRequest`].
pub fn set_cuid(&mut self, cuid: ConfigUid) {
self.aad.cuid = cuid;
self.aad.set_cuid(cuid);
}
/// Sets the extension secret of this [`AddSecretRequest`].
@@ -182,7 +379,7 @@ impl AddSecretRequest {
ExtSecret::Derived(cck) => hkdf_rfc_5869(
Md::sha512(),
cck.value(),
self.aad.boot_tags.tag(),
self.aad.boot_tags().tag(),
DER_EXT_SECRET_INFO,
)?
.into(),
@@ -221,7 +418,6 @@ impl AddSecretRequest {
let cust_pub_key = ctx.key_coords()?;
let secr_auth = self.conf.secret.auth();
let user_data = self.user_data.data();
let mut aad: Vec<Aad> = Vec::with_capacity(5 + self.keyslots.len());
aad.push(Aad::Plain(self.aad.as_bytes()));
if let Some(data) = user_data.0 {
@@ -232,7 +428,13 @@ impl AddSecretRequest {
}
aad.push(Aad::Plain(cust_pub_key.as_ref()));
self.keyslots.iter().for_each(|k| aad.push(Aad::Ks(k)));
aad.push(Aad::Plain(secr_auth.get()));
// write secret header (1) only for v1
match self.version {
AddSecretVersion::One => aad.push(Aad::Plain(secr_auth.get(AddSecretVersion::One))),
AddSecretVersion::Two => {}
#[cfg(any(debug_assertions, test))]
_ => return Err(Error::UnsupportedAddSecretVersion(self.version as u32)),
}
ctx.build_aad(self.version.into(), &aad, conf_len, self.user_data.magic())
}
@@ -273,7 +475,7 @@ impl AddSecretRequest {
// add signature to authenticated data starting with USER_DATA_OFFS
self.user_data.sign(
&mut buf[aad_range.start..encr_range.end],
Self::V1_USER_DATA_OFFS,
Self::USER_DATA_OFFS,
)?;
// encrypt again with signed data
@@ -309,7 +511,101 @@ impl Request for AddSecretRequest {
}
}
fn add_hostkey(&mut self, hostkey: HostKey) {
self.keyslots.push(Keyslot::new(hostkey))
fn add_hostkey(&mut self, hostkey: HostKey) -> Result<()> {
match self.version {
AddSecretVersion::One if !hostkey.is_hybrid() => Ok(()),
AddSecretVersion::Two if hostkey.is_hybrid() => Ok(()),
AddSecretVersion::One => Err(Error::InvalidHkd(
"Add classical hostkey to a v1 attestation request".to_string(),
)),
AddSecretVersion::Two => Err(Error::InvalidHkd(
"Add hybrid key to a v2 attetstation request".to_string(),
)),
#[cfg(any(debug_assertions, test))]
AddSecretVersion::Inv => panic!("Invalid version for production use"),
}?;
self.keyslots.push(Keyslot::new(hostkey));
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::request::SeHdrVersion;
#[test]
fn add_secret_version_v1() {
// Test V1 version constant
assert_eq!(AddSecretVersion::One as u32, 0x0100);
}
#[test]
fn add_secret_version_v2() {
// Test V2 version constant
assert_eq!(AddSecretVersion::Two as u32, 0x0200);
}
#[test]
fn add_secret_version_conversion_v1() {
// Test conversion from SeHdrVersion to AddSecretVersion for V1
let v1: AddSecretVersion = SeHdrVersion::One.into();
assert_eq!(v1, AddSecretVersion::One);
}
#[test]
fn add_secret_version_conversion_v2() {
// Test conversion from SeHdrVersion to AddSecretVersion for V2
let v2: AddSecretVersion = SeHdrVersion::Two.into();
assert_eq!(v2, AddSecretVersion::Two);
}
#[test]
fn add_secret_version_into_request_version() {
// Test conversion from AddSecretVersion to RequestVersion
let v1: RequestVersion = AddSecretVersion::One.into();
assert_eq!(v1, 0x0100);
let v2: RequestVersion = AddSecretVersion::Two.into();
assert_eq!(v2, 0x0200);
}
#[test]
fn add_secret_flags_default() {
// Test default flags have no bits set
let flags = AddSecretFlags::default();
let uv_flags: UvFlags = flags.into();
// Default should have all bits cleared
assert_eq!(uv_flags.as_bytes(), &[0u8; 8]);
}
#[test]
fn add_secret_flags_disable_dump() {
// Test disable dump flag sets bit 0
let mut flags = AddSecretFlags::default();
flags.set_disable_dump();
let uv_flags: UvFlags = flags.into();
// Bit 0 should be set, so bytes should not be all zeros
assert_ne!(uv_flags.as_bytes(), &[0u8; 8]);
}
#[test]
fn req_auth_data_size() {
// Test ReqAuthData size constant
use std::mem::size_of;
assert_eq!(size_of::<ReqAuthDataV1>(), 0x1e8);
}
#[test]
#[cfg(any(debug_assertions, test))]
fn add_secret_version_inv_for_testing() {
// Test that invalid version exists for testing
assert_eq!(AddSecretVersion::Inv as u32, 0);
assert_ne!(AddSecretVersion::Inv, AddSecretVersion::One);
assert_ne!(AddSecretVersion::Inv, AddSecretVersion::Two);
}
}

View File

@@ -16,6 +16,7 @@ use super::asrcb::AddSecretRequest;
use crate::crypto::{hash, random_array, SymKeyType};
use crate::request::openssl::{NID_ED25519, NID_ED448};
use crate::request::Confidential;
use crate::secret::AddSecretVersion;
use crate::uv::{
AesSizes, AesXtsSizes, EcCurves, HmacShaSizes, ListableSecretType, RetrievableSecret,
RetrieveCmd, SecretId,
@@ -388,12 +389,18 @@ pub(crate) enum SecretAuth {
}
impl SecretAuth {
const NULL_HDR: NullSecretHdr = NullSecretHdr::new();
const NULL_HDR_V1: NullSecretHdrV1 = NullSecretHdrV1::new();
const NULL_HDR_V2: NullSecretHdrV2 = NullSecretHdrV2::new();
const UPDATE_CCK_HDR: UpdateCckHdr = UpdateCckHdr::new();
pub fn get(&self) -> &[u8] {
pub fn get(&self, version: AddSecretVersion) -> &[u8] {
match self {
Self::Null => Self::NULL_HDR.as_bytes(),
Self::Null => match version {
AddSecretVersion::One => Self::NULL_HDR_V1.as_bytes(),
AddSecretVersion::Two => Self::NULL_HDR_V2.as_bytes(),
#[cfg(any(debug_assertions, test))]
_ => panic!("Invalid AddSecretVersion"),
},
Self::Listable(h) => h.as_bytes(),
Self::UpdateCck => Self::UPDATE_CCK_HDR.as_bytes(),
}
@@ -402,15 +409,15 @@ impl SecretAuth {
#[repr(C)]
#[derive(Debug, IntoBytes, FromBytes, Immutable, KnownLayout)]
struct NullSecretHdr {
struct NullSecretHdrV1 {
res0: u16,
kind: U16<BigEndian>,
secret_len: U32<BigEndian>,
res8: u64,
}
assert_size!(NullSecretHdr, 0x10);
assert_size!(NullSecretHdrV1, 0x10);
impl NullSecretHdr {
impl NullSecretHdrV1 {
const fn new() -> Self {
Self {
res0: 0,
@@ -421,6 +428,29 @@ impl NullSecretHdr {
}
}
#[repr(C)]
#[derive(Debug, IntoBytes, FromBytes, Immutable, KnownLayout)]
struct NullSecretHdrV2 {
res0: u16,
kind: U16<BigEndian>,
secret_len: U32<BigEndian>,
res8: u64,
reserved1: [u8; 32],
}
assert_size!(NullSecretHdrV2, 0x30);
impl NullSecretHdrV2 {
const fn new() -> Self {
Self {
res0: 0,
kind: U16::new(ListableSecretType::NULL),
secret_len: U32::ZERO,
res8: 0,
reserved1: [0; 32],
}
}
}
#[repr(C)]
#[derive(Debug, IntoBytes, FromBytes, Immutable, KnownLayout)]
pub(crate) struct ListableSecretHdr {
@@ -701,7 +731,7 @@ mod test {
let gs_bytes = gs.auth();
let exp = vec![0u8, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
assert_eq!(exp, gs_bytes.get());
assert_eq!(exp, gs_bytes.get(AddSecretVersion::One));
assert_eq!(&Vec::<u8>::new(), gs.confidential())
}
@@ -716,7 +746,7 @@ mod test {
let mut exp = vec![0u8, 0, 0, 2, 0, 0, 0, 0x20, 0, 0, 0, 0, 0, 0, 0, 0];
exp.extend([1; 32]);
assert_eq!(exp, gs_bytes_auth.get());
assert_eq!(exp, gs_bytes_auth.get(AddSecretVersion::One));
assert_eq!(&[2; 32], gs.confidential());
}
@@ -729,7 +759,7 @@ mod test {
secret: vec![2; 32].into(),
};
let auth = gs.auth();
let gs_bytes_auth = auth.get();
let gs_bytes_auth = auth.get(AddSecretVersion::One);
let mut exp = vec![0u8, 0, 0, 3, 0, 0, 0, 0x20, 0, 0, 0, 0, 0, 0, 0, 0];
exp.extend([1; 32]);
@@ -746,7 +776,7 @@ mod test {
let mut exp = vec![0u8, 0, 0, 0x16, 0, 0, 0, 0x20];
exp.extend([0; 40]);
assert_eq!(exp, gs_bytes_auth.get());
assert_eq!(exp, gs_bytes_auth.get(AddSecretVersion::One));
assert_eq!(&[2; 32], gs.confidential());
}
}

View File

@@ -133,6 +133,7 @@ impl RetrievedSecret {
RetrievedSecret::ProtectedKey(p) => p.into_bytes(),
}
}
/// Get the data in PEM format.
///
/// # Errors

View File

@@ -10,7 +10,7 @@ use crate::crypto::{sign_msg, verify_signature};
use crate::req::BinReqValues;
use crate::request::openssl::pkey::{HasParams, HasPublic, Id, PKey, PKeyRef, Private, Public};
use crate::request::RequestMagic;
use crate::secret::{AddSecretMagic, AddSecretRequest, AddSecretVersion, UserDataType};
use crate::secret::{AddSecretMagic, AddSecretRequest, UserDataType};
use crate::{assert_size, Error, Result};
/// User data.
@@ -261,21 +261,19 @@ pub fn verify_asrcb_and_get_user_data(
// check that the provided buffer contains an Add Secret request
let magic = AddSecretMagic::try_from_bytes(&asrcb)?;
let req = BinReqValues::get(&asrcb)?;
if req.version() != AddSecretVersion::One as u32 {
return Err(Error::BinAsrcbInvVersion);
}
// preventing the two lines after the truncate from panicking
let req_len = req.len();
if asrcb.len() < req_len
|| req_len < AddSecretRequest::V1_USER_DATA_OFFS + UserData::USER_DATA_SIZE
|| req_len < AddSecretRequest::USER_DATA_OFFS + UserData::USER_DATA_SIZE
{
return Err(pv_core::Error::NoAsrcb.into());
}
// forget the tag (and all additional data that might be behind the tag)
asrcb.truncate(req_len - BinReqValues::TAG_LEN);
// get a mutable refrenence on the 512 bytes of user data
let (_, user_data) = asrcb.split_at_mut(AddSecretRequest::V1_USER_DATA_OFFS);
let (_, user_data) = asrcb.split_at_mut(AddSecretRequest::USER_DATA_OFFS);
let user_data = &mut user_data[..UserData::USER_DATA_SIZE];
// depending on the user_data_type do:

View File

@@ -6,13 +6,13 @@
use openssl::ec::{EcGroup, EcKey};
use openssl::nid::Nid;
use openssl::pkey::Private;
use s390_pv::request::openssl::pkey::{PKey, Public};
use s390_pv::request::{BootHdrTags, HostKey, ReqEncrCtx, Request, SymKey};
use s390_pv::request::openssl::pkey::PKey;
use s390_pv::request::{BootHdrTags, HostKey, HybridPKey, ReqEncrCtx, Request, SymKey};
use s390_pv::secret::{
verify_asrcb_and_get_user_data, AddSecretFlags, AddSecretRequest, AddSecretVersion, ExtSecret,
GuestSecret,
};
use s390_pv::test_utils::get_test_keys;
use s390_pv::test_utils::{get_test_keys, get_test_keys_hybrid, DeterministicTestRandGuard};
use s390_pv::uv::ConfigUid;
use s390_pv::{get_test_asset, Result};
@@ -29,8 +29,13 @@ fn create_asrcb(
hkd: HostKey,
ctx: &ReqEncrCtx,
) -> Result<Vec<u8>> {
let mut asrcb = AddSecretRequest::new(AddSecretVersion::One, guest_secret, TAGS, flags);
let asrcb = match hkd {
HostKey::V1(_) => AddSecretRequest::new(AddSecretVersion::One, guest_secret, TAGS, flags),
HostKey::V2(_) => AddSecretRequest::new(AddSecretVersion::Two, guest_secret, TAGS, flags),
_ => unreachable!("Unknown HostKey version"),
};
let mut asrcb = asrcb?;
if let Some(s) = ext_secret {
asrcb.set_ext_secret(s)?
};
@@ -38,12 +43,25 @@ fn create_asrcb(
asrcb.set_cuid(c);
};
asrcb.add_hostkey(hkd);
asrcb.add_hostkey(hkd)?;
asrcb.encrypt(ctx)
}
fn get_crypto() -> (PKey<Public>, ReqEncrCtx) {
fn get_crypto() -> (HostKey, ReqEncrCtx) {
let (cust_key, host_key) = get_test_keys();
let host_key = HostKey::V1(host_key);
let ctx = ReqEncrCtx::new_aes_256(
Some([0x55; 12]),
Some(cust_key),
Some(SymKey::Aes256([0x17; 32].into())),
)
.unwrap();
(host_key, ctx)
}
fn get_crypto_v2() -> (HostKey, ReqEncrCtx) {
let (cust_key, host_key1, host_key2) = get_test_keys_hybrid();
let host_key = HostKey::V2(HybridPKey::new(host_key1, host_key2).unwrap());
let ctx = ReqEncrCtx::new_aes_256(
Some([0x55; 12]),
Some(cust_key),
@@ -67,14 +85,25 @@ where
true => Some(CUID),
false => None,
};
create_asrcb(
guest_secret,
ext_secret.into(),
flags,
cuid,
HostKey::V1(host_key),
&ctx,
)
create_asrcb(guest_secret, ext_secret.into(), flags, cuid, host_key, &ctx)
}
fn gen_asrcb_v2<E>(
guest_secret: GuestSecret,
ext_secret: E,
flags: AddSecretFlags,
cuid: bool,
) -> Result<Vec<u8>>
where
E: Into<Option<ExtSecret>>,
{
let _guard = DeterministicTestRandGuard::install(&[0x42; 4096], &[0x11; 32]).unwrap();
let (host_key, ctx) = get_crypto_v2();
let cuid = match cuid {
true => Some(CUID),
false => None,
};
create_asrcb(guest_secret, ext_secret.into(), flags, cuid, host_key, &ctx)
}
fn association() -> GuestSecret {
@@ -96,9 +125,21 @@ fn no_flag() -> AddSecretFlags {
fn create_signed_asrcb(skey: PKey<Private>, user_data: Vec<u8>) -> Vec<u8> {
let (host_key, ctx) = get_crypto();
let mut asrcb =
AddSecretRequest::new(AddSecretVersion::One, GuestSecret::Null, TAGS, no_flag());
AddSecretRequest::new(AddSecretVersion::One, GuestSecret::Null, TAGS, no_flag())
.expect("AddSecretRequest::new failed");
asrcb.add_hostkey(HostKey::V1(host_key));
asrcb.add_hostkey(host_key).unwrap();
asrcb.set_user_data(user_data, Some(skey)).unwrap();
asrcb.encrypt(&ctx).unwrap()
}
fn create_signed_asrcb_v2(skey: PKey<Private>, user_data: Vec<u8>) -> Vec<u8> {
let (host_key, ctx) = get_crypto_v2();
let mut asrcb =
AddSecretRequest::new(AddSecretVersion::Two, GuestSecret::Null, TAGS, no_flag())
.expect("AddSecretRequest::new failed");
asrcb.add_hostkey(host_key).unwrap();
asrcb.set_user_data(user_data, Some(skey)).unwrap();
asrcb.encrypt(&ctx).unwrap()
}
@@ -108,9 +149,10 @@ fn null_none_default_ncuid_one_user_unsgn() {
let user_data_orig = vec![0x56; 0x183];
let (host_key, ctx) = get_crypto();
let mut asrcb =
AddSecretRequest::new(AddSecretVersion::One, GuestSecret::Null, TAGS, no_flag());
AddSecretRequest::new(AddSecretVersion::One, GuestSecret::Null, TAGS, no_flag())
.expect("AddSecretRequest::new failed");
asrcb.add_hostkey(HostKey::V1(host_key));
asrcb.add_hostkey(host_key).unwrap();
asrcb.set_user_data(user_data_orig.clone(), None).unwrap();
let asrcb = asrcb.encrypt(&ctx).unwrap();
@@ -121,6 +163,7 @@ fn null_none_default_ncuid_one_user_unsgn() {
&user_data.as_ref().unwrap()[..user_data_orig.len()]
);
}
#[test]
fn null_none_default_ncuid_one_user_ec() {
let (usr_sgn_key, _) = get_test_keys();
@@ -242,8 +285,11 @@ fn null_none_default_ncuid_one() {
fn null_none_default_cuid_seven() {
let (hkd, ctx) = get_crypto();
let mut asrcb =
AddSecretRequest::new(AddSecretVersion::One, GuestSecret::Null, TAGS, no_flag());
(0..7).for_each(|_| asrcb.add_hostkey(HostKey::V1(hkd.clone())));
AddSecretRequest::new(AddSecretVersion::One, GuestSecret::Null, TAGS, no_flag())
.expect("AddSecretRequest::new failed");
for _ in 0..7 {
asrcb.add_hostkey(hkd.clone()).unwrap()
}
asrcb.set_cuid(CUID);
let asrcb = asrcb.encrypt(&ctx).unwrap();
@@ -259,3 +305,164 @@ fn verify_no_user_data() {
Ok(None)
))
}
// V2 tests (using hybrid keys)
#[test]
fn null_none_default_ncuid_two_user_unsgn() {
let _guard = DeterministicTestRandGuard::install(&[0x42; 4096], &[0x11; 32]).unwrap();
let user_data_orig = vec![0x56; 0x183];
let (host_key, ctx) = get_crypto_v2();
let mut asrcb =
AddSecretRequest::new(AddSecretVersion::Two, GuestSecret::Null, TAGS, no_flag())
.expect("AddSecretRequest::new failed");
asrcb.add_hostkey(host_key).unwrap();
asrcb.set_user_data(user_data_orig.clone(), None).unwrap();
let asrcb = asrcb.encrypt(&ctx).unwrap();
let user_data = verify_asrcb_and_get_user_data(asrcb, None).unwrap();
assert_eq!(
user_data_orig.as_slice(),
&user_data.as_ref().unwrap()[..user_data_orig.len()]
);
}
#[test]
fn null_none_default_ncuid_two_user_ec() {
let _guard = DeterministicTestRandGuard::install(&[0x42; 4096], &[0x11; 32]).unwrap();
let (usr_sgn_key, _) = get_test_keys();
let usr_vrfy_key = usr_sgn_key.ec_key().unwrap();
let usr_vrfy_key = usr_vrfy_key.public_key();
let usr_vrfy_key = PKey::from_ec_key(
EcKey::from_public_key(
&EcGroup::from_curve_name(Nid::SECP521R1).unwrap(),
usr_vrfy_key,
)
.unwrap(),
)
.unwrap();
let user_data_orig = vec![0x56; 0x100];
let asrcb = create_signed_asrcb_v2(usr_sgn_key, user_data_orig.clone());
let user_data = verify_asrcb_and_get_user_data(asrcb, Some(usr_vrfy_key)).unwrap();
assert_eq!(
user_data_orig.as_slice(),
&user_data.as_ref().unwrap()[..user_data_orig.len()]
);
}
#[test]
fn null_none_default_ncuid_two_user_rsa2048() {
let _guard = DeterministicTestRandGuard::install(&[0x42; 4096], &[0x11; 32]).unwrap();
let usr_sgn_key = get_test_asset!("keys/rsa2048key.pem");
let usr_sgn_key = PKey::private_key_from_pem(usr_sgn_key).unwrap();
let user_data_orig = vec![0x56; 0x100];
let asrcb = create_signed_asrcb_v2(usr_sgn_key, user_data_orig.clone());
let usr_vrfy_key = get_test_asset!("keys/rsa2048key.pub.pem");
let usr_vrfy_key = PKey::public_key_from_pem(usr_vrfy_key).unwrap();
let user_data = verify_asrcb_and_get_user_data(asrcb, Some(usr_vrfy_key)).unwrap();
assert_eq!(
user_data_orig.as_slice(),
&user_data.as_ref().unwrap()[..user_data_orig.len()]
);
}
#[test]
fn null_none_default_ncuid_two_user_rsa3072() {
let _guard = DeterministicTestRandGuard::install(&[0x42; 4096], &[0x11; 32]).unwrap();
let usr_sgn_key = get_test_asset!("keys/rsa3072key.pem");
let usr_sgn_key = PKey::private_key_from_pem(usr_sgn_key).unwrap();
let user_data_orig = vec![0x56; 0x80];
let asrcb = create_signed_asrcb_v2(usr_sgn_key, user_data_orig.clone());
let usr_vrfy_key = get_test_asset!("keys/rsa3072key.pub.pem");
let usr_vrfy_key = PKey::public_key_from_pem(usr_vrfy_key).unwrap();
let user_data = verify_asrcb_and_get_user_data(asrcb, Some(usr_vrfy_key)).unwrap();
assert_eq!(
user_data_orig.as_slice(),
&user_data.as_ref().unwrap()[..user_data_orig.len()]
);
}
#[test]
fn null_none_default_cuid_two() {
let _guard = DeterministicTestRandGuard::install(&[0x42; 4096], &[0x11; 32]).unwrap();
let asrcb = gen_asrcb_v2(GuestSecret::Null, None, no_flag(), true).unwrap();
let exp = get_test_asset!("exp/asrcb/null_none_default_cuid_two");
assert_eq!(asrcb, exp);
}
#[test]
fn assoc_none_default_cuid_two() {
let asrcb = gen_asrcb_v2(association(), None, no_flag(), true).unwrap();
let exp = get_test_asset!("exp/asrcb/assoc_none_default_cuid_two");
assert_eq!(asrcb, exp);
}
#[test]
fn null_simple_default_cuid_two() {
let asrcb = gen_asrcb_v2(GuestSecret::Null, ext_simple(), no_flag(), true).unwrap();
let exp = get_test_asset!("exp/asrcb/null_simple_default_cuid_two");
assert_eq!(asrcb, exp);
}
#[test]
fn assoc_simple_default_cuid_two() {
let asrcb = gen_asrcb_v2(association(), ext_simple(), no_flag(), true).unwrap();
let exp = get_test_asset!("exp/asrcb/assoc_simple_default_cuid_two");
assert_eq!(asrcb, exp);
}
#[test]
fn null_derived_default_cuid_two() {
let asrcb = gen_asrcb_v2(GuestSecret::Null, ext_derived(), no_flag(), true).unwrap();
let exp = get_test_asset!("exp/asrcb/null_derived_default_cuid_two");
assert_eq!(asrcb, exp);
}
#[test]
fn assoc_derived_default_cuid_two() {
let asrcb = gen_asrcb_v2(association(), ext_derived(), no_flag(), true).unwrap();
let exp = get_test_asset!("exp/asrcb/assoc_derived_default_cuid_two");
assert_eq!(asrcb, exp);
}
#[test]
fn null_none_dump_cuid_two() {
let mut flags = no_flag();
flags.set_disable_dump();
let asrcb = gen_asrcb_v2(GuestSecret::Null, None, flags, true).unwrap();
let exp = get_test_asset!("exp/asrcb/null_none_dump_cuid_two");
assert_eq!(asrcb, exp);
}
#[test]
fn null_none_default_ncuid_two() {
let asrcb = gen_asrcb_v2(GuestSecret::Null, None, no_flag(), false).unwrap();
let exp = get_test_asset!("exp/asrcb/null_none_default_ncuid_two");
assert_eq!(asrcb, exp);
}
#[test]
fn null_none_default_cuid_seven_two() {
let _guard = DeterministicTestRandGuard::install(&[0x42; 4096], &[0x11; 32]).unwrap();
let (hkd, ctx) = get_crypto_v2();
let mut asrcb =
AddSecretRequest::new(AddSecretVersion::Two, GuestSecret::Null, TAGS, no_flag())
.expect("AddSecretRequest::new failed");
for _ in 0..7 {
asrcb.add_hostkey(hkd.clone()).unwrap()
}
asrcb.set_cuid(CUID);
let asrcb = asrcb.encrypt(&ctx).unwrap();
let exp = get_test_asset!("exp/asrcb/null_none_default_cuid_seven_two");
assert_eq!(asrcb, exp);
}

Binary file not shown.

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -euo pipefail
OUT_PREFIX=${1:-host}
SUBJ="/C=US/O=IBM/OU=Testing/CN=Qsafe HK"
if [ -f "${OUT_PREFIX}.hybrid.crt" ]; then
echo "[*] Nothing to do... ${OUT_PREFIX}.hybrid.crt already exists"
exit 0
fi
echo "[*] Generating ML-KEM-1024 keypair..."
if openssl list -kem-algorithms 2>/dev/null | grep -qi ML-KEM-1024; then
echo " -> OpenSSL OQS provider detected"
if ! [ -f "${OUT_PREFIX}.mlkem.key" ]; then
echo "[*] Generate ML-KEM key (target public key)"
openssl genpkey -algorithm MLKEM1024 -out "${OUT_PREFIX}.mlkem.key"
openssl pkey -in "${OUT_PREFIX}.mlkem.key" -pubout -out "${OUT_PREFIX}.mlkem.pub.pem"
fi
if ! [ -f "issuer.key" ] || ! [ -f "issuer.crt" ]; then
echo "[*] Generate RSA issuer (signing key)"
openssl req -x509 -newkey rsa:4096 -nodes \
-keyout issuer.key \
-out issuer.crt \
-days 365 \
-subj "${SUBJ}"
fi
if ! [ -f "${OUT_PREFIX}.mlkem.crt" ]; then
echo "[*] Create certificate with injected ML-KEM public key"
openssl req -new -newkey rsa:2048 -nodes \
-keyout tmp.key \
-subj "${SUBJ}" |
openssl x509 -req \
-CA issuer.crt \
-CAkey issuer.key \
-CAcreateserial \
-days 365 \
-set_serial 0x66A376DBB5502C74C6 \
-force_pubkey "${OUT_PREFIX}.mlkem.pub.pem" \
-extfile <(
cat <<EOF
basicConstraints=CA:FALSE
keyUsage=critical,keyAgreement
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid
crlDistributionPoints=URI:http://issuer.crl
EOF
) \
-out "${OUT_PREFIX}.mlkem.crt"
fi
else
echo " -> No PQ support, using placeholder"
exit 1
fi
echo "[*] Building hybrid PEM: ${OUT_PREFIX}.hybrid.pem"
{
echo "-----BEGIN CERTIFICATE-----"
sed '/-----/d' "${OUT_PREFIX}.pem.crt"
echo "-----END CERTIFICATE-----"
echo "-----BEGIN CERTIFICATE-----"
sed '/-----/d' "${OUT_PREFIX}.mlkem.crt"
echo "-----END CERTIFICATE-----"
} >"${OUT_PREFIX}.hybrid.crt"
echo "[✓] Done"
echo " -> ${OUT_PREFIX}.hybrid.crt"

View File

@@ -0,0 +1,15 @@
-----BEGIN CERTIFICATE-----
MIICXDCCAb6gAwIBAgIUcOg3S9/SHLdnpu6iaI7QI2cszsgwCgYIKoZIzj0EAwIw
QDELMAkGA1UEBhMCVVMxDDAKBgNVBAoMA0lCTTEQMA4GA1UECwwHVGVzdGluZzER
MA8GA1UEAwwIUXNhZmUgSEswHhcNMjYwNjI1MTM1MTE4WhcNMzYwNjIyMTM1MTE4
WjBAMQswCQYDVQQGEwJVUzEMMAoGA1UECgwDSUJNMRAwDgYDVQQLDAdUZXN0aW5n
MREwDwYDVQQDDAhRc2FmZSBISzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAfB0
HaSx/tEDOnA0tv/tSFc2v1t0YarC8L9QcV0bU+o4Gk6OBriQi0GbUna4CndAPWrr
BYvC/ZVd2nKqZhx5WGKNAAq/NObSdvH6uNfjZY27pS56fB2abxa5UzlH/KkYAU+J
m1sFjZhOqD1Bvb/mizhz1WqdHucGSQYO55WDL+KH06Goo1MwUTAdBgNVHQ4EFgQU
/n03CIsLupLkHh5OwcdJFfucD8IwHwYDVR0jBBgwFoAU/n03CIsLupLkHh5OwcdJ
FfucD8IwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgOBiwAwgYcCQTTEnxAy
8/inrBEMtAINb9yA+LAXk5mDQIa7WXOiKRiJKLqHyrjPXqMIAtNgWwSmLggHiIYE
R4MiacrfbVdEOda3AkIBLv0XRN8zEaMLDlis6EdhgF+/AXFtsYxsjafZpoqnfx2X
YfbIgFpLUTcYWwoPi5uopbiAwv3AQy3x5CEFwG3e5PY=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,8 @@
-----BEGIN PRIVATE KEY-----
MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAKog0FWiwlXuAS1jK
dNsahw9KZ+r4zvZrp1LmDSOQNOfkauXHH1EmBEnUkYccsi7PibYkz80sxbtk+NVW
Kk6pGk2hgYkDgYYABAHwdB2ksf7RAzpwNLb/7UhXNr9bdGGqwvC/UHFdG1PqOBpO
jga4kItBm1J2uAp3QD1q6wWLwv2VXdpyqmYceVhijQAKvzTm0nbx+rjX42WNu6Uu
enwdmm8WuVM5R/ypGAFPiZtbBY2YTqg9Qb2/5os4c9VqnR7nBkkGDueVgy/ih9Oh
qA==
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,84 @@
-----BEGIN CERTIFICATE-----
MIIFFDCCAvygAwIBAgIUO+SNGBgRvkmR/lKiZhiN4l4zS70wDQYJKoZIhvcNAQEN
BQAwgcwxCzAJBgNVBAYTAlVTMTQwMgYDVQQKDCtJbnRlcm5hdGlvbmFsIEJ1c2lu
ZXNzIE1hY2hpbmVzIENvcnBvcmF0aW9uMTQwMgYDVQQDDCtJbnRlcm5hdGlvbmFs
IEJ1c2luZXNzIE1hY2hpbmVzIENvcnBvcmF0aW9uMREwDwYDVQQIDAhOZXcgWW9y
azEVMBMGA1UEBwwMUG91Z2hrZWVwc2llMScwJQYDVQQLDB5JQk0gWiBIb3N0IEtl
eSBTaWduaW5nIFNlcnZpY2UwIBcNMjMwMzI5MDkwNDQ4WhgPMjM4NzEyMzEwOTA0
NDhaMIG2MQswCQYDVQQGEwJVUzE0MDIGA1UECgwrSW50ZXJuYXRpb25hbCBCdXNp
bmVzcyBNYWNoaW5lcyBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrSW50ZXJuYXRpb25h
bCBCdXNpbmVzcyBNYWNoaW5lcyBDb3Jwb3JhdGlvbjERMA8GA1UECAwITmV3IFlv
cmsxDzANBgNVBAcMBkFybW9uazEXMBUGA1UECwwOSUJNIFogSG9zdCBLZXkwgZsw
EAYHKoZIzj0CAQYFK4EEACMDgYYABAF2iBzYS4tt5xI8fpAO33jn97aEmZFGsSY7
qLhshEfwirbUacKxKO2eHDUBWAWs09MCM9ORIvfi+KocKxR7eIO0BgDDRVbCXPkv
Kc5mvUQRTWt7PHjhOj+QvbkAQPaHblJi93imGpN9GzSALrJX404Gct1fKjR63aoo
IDUJKnDDXC55c6OBhzCBhDAyBgNVHR8EKzApMCegJaAjhiFodHRwOi8vMTI3LjAu
MC4xOjEyMzQvY3JsL2libS5jcmwwDgYDVR0PAQH/BAQDAgMIMB8GA1UdIwQYMBaA
FN6M1/Do0NgBB3F9b9loWSA+sGd8MB0GA1UdDgQWBBR6ESQ+J+iXQe0a/KZ2+N+Z
8a9SZjANBgkqhkiG9w0BAQ0FAAOCAgEAVWJHoCxxkqh7b+y93PY4KPy4jJC4gN1S
dAPqttD/yteJ/4mbVel1/KNSoQBk5EpJmRqeBwHCgGJaT/TxYXNw/b8mRPxe/xbb
wieZMqlSmH028UjYDku1eM0IgHISgoCesIR95D5iAOWbMMVUwIHIHTfmhK7DmZVe
SPf7RIkctrpYxZh0Gw8KLZO6Mfy/9tq3dps0A7KS6jjdrF+M9LavPGwFvtfvRMTi
rdteByO2saGAKDvrjtievwlWCNBJlKV1arW9krN7eqJY5YO6eRbX6UjuhbPRgjte
eZ4jL121TBJaKZU7Q/lvYHIWfzstwQdiem2Ua1GyiiEvPZrQlmqQ3gDBtwJQGB4Q
2myP7MY7THiKObjaB8qRsVxKM78ktwAtAYSZv7gZlmSJ/uTMzDV5D2TQxqs7zwCj
sV+psUn4nvh58xP+DW+MYbF/Cpmzvul9FjMKBs270vE1q+gMot27rbQHRRJ4lVN5
khiG6Oi6blVkPKExIVIiaZ9diXK6NhtWp15PWljNiDxZO+zpkeuw7cKLn/idzmvP
Gcj6m7DqcdsSIHNKbR5iM2VuDhg/j8uBD3uF2Wlymp31TBQgdYWSihJpwZKHNqJD
uq9SmegwI5gYg64KwABZM9hGbl/krXt/0CeCR5HRc+fthanKx/tO2tbCuVT3FR5J
XpKNUy1D78o=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
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
jq+SvDvKMqeV
-----END CERTIFICATE-----

View File

@@ -0,0 +1,54 @@
-----BEGIN CERTIFICATE-----
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
jq+SvDvKMqeV
-----END CERTIFICATE-----

View File

@@ -0,0 +1,71 @@
-----BEGIN PRIVATE KEY-----
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
+JA=
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,36 @@
-----BEGIN PUBLIC KEY-----
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
pJafxe8R
-----END PUBLIC KEY-----

View File

@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFYTCCA0mgAwIBAgIUIDYNUFtHABjQvM11wfnJrL2T9v8wDQYJKoZIhvcNAQEL
BQAwQDELMAkGA1UEBhMCVVMxDDAKBgNVBAoMA0lCTTEQMA4GA1UECwwHVGVzdGlu
ZzERMA8GA1UEAwwIUXNhZmUgSEswHhcNMjYwNzEzMDg1MTU4WhcNMjcwNzEzMDg1
MTU4WjBAMQswCQYDVQQGEwJVUzEMMAoGA1UECgwDSUJNMRAwDgYDVQQLDAdUZXN0
aW5nMREwDwYDVQQDDAhRc2FmZSBISzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
AgoCggIBAJ9mCfJnjEYsXB6ORAxZtbbvIfAt5+EyG/bhSDhy2O2LH+VSwnrrPDCa
TSp0xlU6tXgJVlIPBH6vKE5cUkJErWGfxtE/nmuhy1j3DnSCN7cGjcAk62iV00bh
DwBpTqDlhqtAxpEFVE04yqLhxblh0ZaWcxhSNFCDFoD5VUnGwuSwxvWcMudd8LzO
XhJDLePUwXDpFa4mhnff0kUMe6VXLHd9QvPIvE7XUEUR5jncjTOD5inu/eyBcYpK
5kqJdBY97DduUt1dtS780p2rPS/HExuSo0qvfeVdt9lo/amugEMqVGFYXsAH6KRz
SpBFRDfd9MGmbX7xOe/cscNx87CjSdwQ8k8frH3xfhaHcqvq0E67WzRP6DGPsytj
26IXIVfpkCVrcfVNUTHPqrTOXjIKje2WbvTwmJv2t+xgwEHuREmrL133BiG1QPwX
GMDYxp1u/RsVvi6k/yX0NMVz8UxRDfhMIMKeU+tiyNL8t4/hikVapYTbvJGUussZ
zIRgbxlEqqPicQH4fdJcWMi+tB1wSyN4wLd93emcb09fVrc95G8LTnJ32UgjXnl2
V5MHCIgCYIzSaf/+u/uISfQQ5AT5F5eVoVWpN+t2X9cMgFyvL2+fcrkUAYmRbMGN
fm0k7+svquXmDEzPUxIR9BGWTYxwklb3uOdZENiTumqKsUSM7dqhAgMBAAGjUzBR
MB0GA1UdDgQWBBQLbGItsikIfnLb8EIztBn5jfRgQjAfBgNVHSMEGDAWgBQLbGIt
sikIfnLb8EIztBn5jfRgQjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA
A4ICAQCNPAKyNyD+m6tBmfCvXd1kFj1dKZUrgPEuPkpnQk01n6eR12pNXAnUcDDx
HAJM/IFC69zZrekDaFV9lQ5tA5IiZefG09srPHSmEmsISBcDGBJjUx9qZNlKI3R+
gVchEb812PstD5ErY9qFUByrOKMTWLSUVoUS7k8IMNGHMELbl2mQlvc/sIYD8wt5
3/W93OQKnemlqEtC1ljCaDPX3T/eCH5qI+VBiC9OXY7lhUujI0Cs0yJAZ/2G7vcM
NIoCIKmZ+C5KKH6r8aG9NE+ciG2yQCvi7J8OWaesJoCLVZxygEYgorIJNxRTcCYH
En1sDGnQR/NKJPcpDT8KNXMriMmzMDXzecSCuE/tE8tL0lbAnncxJ+AlHAWlt09G
x7rSD3v+Axq5oC0S396ctdwgPwETP5FgLvJSuGicXROsUS4VmPW42h0BovMPI/TF
AfjBnKbgQ7aqppeb9nK9JGd7FTLDv1V2iaYsKUP2A2qmAuQVPrSuckNHt+V+xFlC
nkJA0RNUh3XKTFNSNXJtOVpGsr6/kDttnrlKKwDyzk7LAfbMld4RkNV29SSdEY7z
hGmimwe1ogcRYYqVevG1OcVUrugDaj+Ujkvs918VMS7RUW+IOdrjItLgvNKdjjgm
ARZcI7R0xiEH5+CGCTTurP83RIjPfnyuA1A5GRrbVGyE84Wuhg==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
YFF7USKdWC1isfkk3OmsHvIwXzfRUA==
-----END PRIVATE KEY-----

View File

@@ -5,7 +5,10 @@
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum, ValueHint};
use utils::{CertificateOptions, DeprecatedVerbosityOptions};
use utils::{
AutoOrExplicit, CertificateOptions, DeprecatedVerbosityOptions, HkdVersion, ValueEnumDisplay,
ValueEnumFromStr,
};
/// create, perform, and verify attestation measurements
///
@@ -67,6 +70,28 @@ pub enum Command {
Version,
}
/// Secure Execution attestation version for CLI
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, ValueEnumDisplay, ValueEnumFromStr)]
pub enum AttVersion {
#[value(name = "1")]
/// Version 1 - uses traditional cryptographic keys
V1,
#[value(name = "2")]
/// Version 2 - uses hybrid (post-quantum) cryptographic keys
V2,
}
pub type AttVersionSelection = AutoOrExplicit<AttVersion>;
impl From<AttVersion> for HkdVersion {
fn from(val: AttVersion) -> Self {
match val {
AttVersion::V1 => Self::Classical,
AttVersion::V2 => Self::Hybrid,
}
}
}
#[derive(Args, Debug)]
pub struct CreateAttOpt {
#[command(flatten)]
@@ -94,6 +119,10 @@ pub struct CreateAttOpt {
value_delimiter = ','
)]
pub add_data: Vec<AttAddFlags>,
/// Specify the Attestation Request version to use.
#[arg(long = "att-version", value_name = "VERSION", default_value_t = AttVersion::V1)]
pub att_version: AttVersion,
}
#[derive(Debug, ValueEnum, Clone, Copy)]

View File

@@ -12,7 +12,7 @@ pub use check::check;
pub use create::create;
pub use verify::verify;
pub const CMD_FN: &[&str] = &["+create", "+verify"];
pub const CMD_FN: &[&str] = &["+create", "+verify", "+quantumsafe"];
// s390 branch
#[cfg(target_arch = "s390x")]
mod uv_cmd {

View File

@@ -6,10 +6,9 @@ use std::fmt::Display;
use std::path::Path;
use anyhow::Result;
use log::{debug, info};
use log::{debug, info, warn};
use pv::misc::{read_certs, read_file};
use pv::request::openssl::DigestBytes;
use pv::request::EcPubKeyCoord;
use pv::request::{EcPubKeyCoord, HybridPKey, KeyslotV2};
use serde::Serialize;
use utils::HexSlice;
@@ -36,7 +35,7 @@ impl Display for HkCheck {
}
}
fn load_host_keys<A: AsRef<Path>>(hkds: &[A]) -> Result<Vec<(&Path, DigestBytes)>> {
fn load_host_keys<A: AsRef<Path>>(hkds: &[A]) -> Result<Vec<(&Path, Vec<u8>)>> {
let mut hkd_hash = Vec::with_capacity(hkds.len());
for hkd in hkds {
let hkd = hkd.as_ref();
@@ -45,21 +44,41 @@ fn load_host_keys<A: AsRef<Path>>(hkds: &[A]) -> Result<Vec<(&Path, DigestBytes)
hkd: hkd.display().to_string(),
source,
})?;
let ec_coord: EcPubKeyCoord = certs.first().unwrap().public_key()?.as_ref().try_into()?;
hkd_hash.push((hkd, ec_coord.sha256()?));
let ec_key = certs.first().unwrap().public_key()?;
match certs.len() {
1 => {
let ec_coord: EcPubKeyCoord = ec_key.as_ref().try_into()?;
hkd_hash.push((hkd, ec_coord.sha256()?.as_ref().to_vec()));
}
2 => {
let mlkem_key = certs[1].public_key()?;
let ks = KeyslotV2::new(HybridPKey::new(ec_key, mlkem_key)?);
let hash = ks.sha512()?;
hkd_hash.push((hkd, hash[..32].to_vec()));
}
_ => {
warn!(
"The host-key document in '{}' contains more than two certificates!",
hkd.display()
);
Err(pv::Error::WrongNumberOfKeys(hkd.display().to_string()))?;
}
}
}
Ok(hkd_hash)
}
fn contains_phkh<'a>(
hkd_hashes: &[(&'a Path, DigestBytes)],
hkd_hashes: &[(&'a Path, Vec<u8>)],
phkh: &HexSlice<'_>,
mode: HkCheck,
check_enforced: bool,
) -> CheckState<HostKeyCheck<'a>> {
let hk: Vec<_> = hkd_hashes
.iter()
.filter_map(|(path, hash)| match hash.as_ref() == phkh.as_ref() {
.filter_map(|(path, hash)| match hash == phkh.as_ref() {
true => Some(*path),
false => None,
})

View File

@@ -10,7 +10,7 @@ use pv::attest::{AttestationFlags, AttestationMeasAlg, AttestationRequest, Attes
use pv::misc::{create_file, write_file};
use pv::request::{HostKey, ReqEncrCtx, Request, SymKey, SymKeyType};
use crate::cli::{AttAddFlags, CreateAttOpt};
use crate::cli::{AttAddFlags, AttVersion, AttVersionSelection, CreateAttOpt};
use crate::exchange::{ExchangeFormatRequest, ExchangeFormatVersion};
fn flags(cli_flags: &[AttAddFlags]) -> AttestationFlags {
@@ -26,18 +26,57 @@ fn flags(cli_flags: &[AttAddFlags]) -> AttestationFlags {
att_flags
}
/// Auto-detect the attestation version based on the host keys.
///
/// Returns Two if any host key is a hybrid key, otherwise returns V1.
fn auto_detect_version(host_keys: &[HostKey]) -> AttestationVersion {
let use_hybrid_keys = host_keys.iter().any(|k: &HostKey| k.is_hybrid());
if use_hybrid_keys {
AttestationVersion::Two
} else {
AttestationVersion::One
}
}
impl From<AttVersion> for AttestationVersion {
fn from(value: AttVersion) -> Self {
match value {
AttVersion::V1 => Self::One,
AttVersion::V2 => Self::Two,
}
}
}
/// Determine the attestation version to use.
///
/// If an explicit version is provided via CLI, use that.
/// Otherwise, auto-detect based on the host key types.
fn determine_version(
cli_version: AttVersionSelection,
host_keys: &[HostKey],
) -> AttestationVersion {
match cli_version {
AttVersionSelection::Auto => auto_detect_version(host_keys),
AttVersionSelection::Explicit(att_version) => att_version.into(),
}
}
pub fn create(opt: &CreateAttOpt) -> Result<ExitCode> {
let att_version = AttestationVersion::One;
let hkds = opt.certificate_args.get_verified_hkds(
"attestation request",
AttVersionSelection::Explicit(opt.att_version).map(|v| v.into()),
)?;
let att_version = determine_version(AttVersionSelection::Explicit(opt.att_version), &hkds);
let meas_alg = AttestationMeasAlg::HmacSha512;
let mut arcb = AttestationRequest::new(att_version, meas_alg, flags(&opt.add_data))?;
debug!("Generated Attestation request");
// Add host-key documents
opt.certificate_args
.get_verified_hkds("attestation request")?
.into_iter()
.for_each(|k| arcb.add_hostkey(HostKey::V1(k)));
for k in hkds.into_iter() {
arcb.add_hostkey(k)?
}
debug!("Added all host-keys");
let encr_ctx =

View File

@@ -11,6 +11,8 @@ use pv::request::MagicValue;
use pv::uv::{AttestationCmd, ConfigUid};
use zerocopy::{BigEndian, ByteOrder, FromBytes, Immutable, IntoBytes, KnownLayout, U32, U64};
use crate::additional;
const INV_EXCHANGE_FMT_ERROR_TEXT: &str = "The input has not the correct format:";
#[repr(C)]
@@ -99,10 +101,18 @@ impl ExchangeFormatV1Hdr {
let measurement_entry = Entry::from_exp(Some(measurement));
let exp_add = match additional {
0 => None,
size => Some(size),
size => {
if size > AttestationCmd::ADDITIONAL_MAX_SIZE {
bail!(
"Additional data size ({}) exceeds maximum allowed size ({})",
size,
AttestationCmd::ADDITIONAL_MAX_SIZE
);
}
Some(size)
}
};
// TODO min and max size check?
let additional_entry = Entry::from_exp(exp_add); //, AttestationCmd::ADDITIONAL_MAX_SIZE, &mut offset);
let additional_entry = Entry::from_exp(exp_add);
let user_entry = Entry::from_none();
let cuid_entry = Entry::from_none();
@@ -530,7 +540,6 @@ impl ExchangeFormatResponse {
"{INV_EXCHANGE_FMT_ERROR_TEXT} Contains no attestation request.",
))?;
// TODO remove unwrap
let measurement = hdr.measurement.read(reader)?.data().ok_or(anyhow!(
"{INV_EXCHANGE_FMT_ERROR_TEXT} Contains no attestation response (Measurement missing).",
))?;
@@ -614,9 +623,12 @@ mod test {
measurement: usize,
additional: usize,
) {
// TODO as 32 checks
let ctx_write = ExchangeFormatRequest::new(arcb, measurement as u32, additional as u32)
.expect("exchange fmt creation");
let ctx_write = ExchangeFormatRequest::new(
arcb,
measurement.try_into().unwrap(),
additional.try_into().unwrap(),
)
.expect("exchange fmt creation");
// let mut out = create_file(path).unwrap();
let mut out = vec![];
@@ -693,6 +705,50 @@ mod test {
ExchangeFormatRequest::new(ARCB.to_vec(), 0, ADDITIONAL.len() as u32).unwrap_err();
}
#[test]
fn test_additional_data_size_validation() {
// Test for TODO 1 fix: Additional data size validation
let arcb = ARCB.to_vec();
// Test with valid size at maximum
let result = ExchangeFormatV1Hdr::new_request(
&arcb,
MEASUREMENT.len() as u32,
AttestationCmd::ADDITIONAL_MAX_SIZE,
);
assert!(
result.is_ok(),
"Maximum additional data size should be accepted"
);
// Test with size exceeding maximum
let result = ExchangeFormatV1Hdr::new_request(
&arcb,
MEASUREMENT.len() as u32,
AttestationCmd::ADDITIONAL_MAX_SIZE + 1,
);
assert!(
result.is_err(),
"Additional data size exceeding maximum should fail"
);
if let Err(e) = result {
let error_msg = e.to_string();
assert!(
error_msg.contains("exceeds maximum"),
"Error should mention exceeding maximum: {}",
error_msg
);
}
// Test with zero size (no additional data)
let result = ExchangeFormatV1Hdr::new_request(&arcb, MEASUREMENT.len() as u32, 0);
assert!(
result.is_ok(),
"Zero additional data size should be accepted"
);
}
#[test]
fn min_req() {
test_read_write_request(

View File

@@ -10,15 +10,18 @@ use std::path::PathBuf;
use std::str::FromStr;
use anyhow::{anyhow, Context, Error};
use clap::{Parser, ValueHint};
use log::{info, warn};
use pv::misc::{decode_hex, open_file, read_certs, read_file, try_parse_u64};
use pv::request::SymKeyType;
use pv::{Error as PvError, Result};
use clap::{Parser, ValueEnum, ValueHint};
use log::info;
use pv::misc::{decode_hex, open_file, read_file, try_parse_u64};
use pv::request::{HostKey, NoVerifyHkd, SymKeyType};
use pv::Result;
use pvimg::misc::PSW;
use pvimg::secured_comp::{ComponentTrait, Layout, SecuredComponentBuilder};
use pvimg::uvdata::{BuilderTrait, SeHdrBuilder, SeHdrVersion};
use utils::{AtomicFile, AtomicFileOperation, HexSlice, PvLogger, VerbosityOptions};
use pvimg::uvdata::{BuilderTrait, SeHdrBuilder, SeHdrControlFlags, SeHdrVersion, SeTarget};
use utils::{
AtomicFile, AtomicFileOperation, HexSlice, HkdLoader, HkdVersionSelection, PvLogger,
VerbosityOptions,
};
/// Converts the hexstring into a byte vector.
///
@@ -87,6 +90,38 @@ impl Display for ComponentArg {
}
}
/// SE-header version selection
#[derive(Debug, Clone, Copy, ValueEnum)]
enum SeHdrVersionArg {
/// SE-header version 1
#[value(name = "1")]
V1,
/// SE-header version 2
#[value(name = "2")]
V2,
}
impl From<SeHdrVersionArg> for SeHdrVersion {
fn from(arg: SeHdrVersionArg) -> Self {
match arg {
SeHdrVersionArg::V1 => SeHdrVersion::V1,
SeHdrVersionArg::V2 => SeHdrVersion::V2,
}
}
}
impl SeHdrVersionArg {
/// Detect the SE header version from the keys.
/// Returns V2 if any key is hybrid, otherwise V1.
pub fn detect<K: AsRef<[HostKey]>>(keys: K) -> Self {
if keys.as_ref().iter().any(|k| !k.is_hybrid()) {
Self::V1
} else {
Self::V2
}
}
}
/// Create a Secure Execution header.
#[derive(Parser, Debug)]
pub struct Args {
@@ -129,6 +164,10 @@ pub struct Args {
#[arg(short, long)]
pub output: PathBuf,
/// SE-header version to build
#[arg(long, value_enum)]
version: Option<SeHdrVersionArg>,
#[clap(flatten)]
pub verbosity: VerbosityOptions,
}
@@ -203,36 +242,26 @@ fn main() -> anyhow::Result<()> {
info!("\n# Creating Secure Execution Header");
let addr = args.psw_addr;
let mask = args.psw_mask;
let mut builder = SeHdrBuilder::new(
SeHdrVersion::V1,
PSW { addr, mask },
secure_comp_builer.finish()?,
)?;
let mut target_pub_keys = vec![];
for hkd_path in args.host_key_documents {
info!(
"Use the file '{}' as a host key document",
hkd_path.display()
);
let hkd_data = read_file(&hkd_path, "host key document")?;
let certs = read_certs(&hkd_data)?;
if certs.is_empty() {
return Err(PvError::NoHkdInFile(hkd_path.display().to_string()).into());
}
if certs.len() > 1 {
warn!("The host key document in '{}' contains more than one certificate! All keys will be used.",
hkd_path.display());
}
for cert in &certs {
target_pub_keys.push(cert.public_key()?);
}
let cert = HkdLoader::load_and_verify(&hkd_path, &NoVerifyHkd, HkdVersionSelection::Auto)?;
target_pub_keys.push(cert);
}
let version: SeHdrVersion = args
.version
.unwrap_or(SeHdrVersionArg::detect(&target_pub_keys))
.into();
let target = SeTarget::from_se_hdr_version(version);
let pcf = SeHdrControlFlags::from_u64(try_parse_u64(&args.pcf, "pcf")?, target, true);
let scf = SeHdrControlFlags::from_u64(try_parse_u64(&args.scf, "scf")?, target, false);
info!("SE-header version ...: {}", version);
let mut builder = SeHdrBuilder::new(version, PSW { addr, mask }, secure_comp_builer.finish()?)?;
builder.add_hostkeys(&target_pub_keys)?;
let pcf = try_parse_u64(&args.pcf, "pcf")?.into();
let scf = try_parse_u64(&args.scf, "scf")?.into();
info!(
"PSW addr ............: {addr:#018x}\n\
PSW mask ............: {mask:#018x}\n\

View File

@@ -13,7 +13,56 @@ use std::string::ToString;
use clap::builder::{PossibleValue, TypedValueParser};
use clap::{Arg, ArgGroup, Args, Command, CommandFactory, Parser, ValueEnum, ValueHint};
use log::warn;
use utils::{CertificateOptions, DeprecatedVerbosityOptions};
use utils::{
AutoOrExplicit, CertificateOptions, DeprecatedVerbosityOptions, HkdVersion, ValueEnumDisplay,
ValueEnumFromStr,
};
/// SE header control flags for CLI
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ValueEnum, ValueEnumDisplay)]
#[value(rename_all = "kebab-case")]
pub enum SeHdrFlagName {
/// Confidential guest dump support
ConfidentialDump,
/// DEA/TDEA PCKMO key encryption support
PckmoDeaTdea,
/// AES PCKMO key encryption support
PckmoAes,
/// ECC PCKMO key encryption support
PckmoEcc,
/// HMAC PCKMO key encryption support
PckmoHmac,
/// Backup target keys support
BackupTargetKeys,
/// CCK-derived extension secret enforcement for add-secret requests
CckExtensionSecretEnforcement,
/// CCK update support
CckUpdate,
/// Image components without encryption
NoComponentEncryption,
}
/// Secure Execution header version for CLI
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, ValueEnumDisplay, ValueEnumFromStr)]
pub enum HdrVersion {
#[value(name = "1")]
/// Version 1 - uses traditional cryptographic keys
V1,
#[value(name = "2")]
/// Version 2 - uses hybrid (post-quantum) cryptographic keys
V2,
}
pub type HdrVersionSelection = AutoOrExplicit<HdrVersion>;
impl From<HdrVersion> for HkdVersion {
fn from(val: HdrVersion) -> Self {
match val {
HdrVersion::V1 => Self::Classical,
HdrVersion::V2 => Self::Hybrid,
}
}
}
/// Create and inspect IBM Secure Execution images.
///
@@ -56,6 +105,28 @@ impl CliOptions {
}
}
/// Defines a requirement rule for CLI validation.
///
/// A requirement specifies that certain flags require a specific option to be present.
/// For example, the `ConfidentialDump` flag requires the `--cck` option.
struct Requirement {
/// The flags that trigger this requirement
flags: &'static [SeHdrFlagName],
/// The option name that must be present (e.g., "cck")
option: &'static str,
/// Whether the required option is present
present: bool,
}
/// Defines a set of flags that cannot be used together.
///
/// When multiple flags from this set are present in the command line,
/// a validation error is raised with a dynamically generated message
/// listing the conflicting flags.
struct MutuallyExclusiveFlags {
flags: &'static [SeHdrFlagName],
}
/// Validates the given command line options.
///
/// # Errors
@@ -71,6 +142,92 @@ pub fn validate_cli(opts: &CliOptions) -> Result<(), clap::error::Error> {
{
warn!("Use bootloader directory: {}", dir.display());
}
// Check that a user provided CCK is available
let rules = [Requirement {
flags: &[
SeHdrFlagName::ConfidentialDump,
SeHdrFlagName::CckExtensionSecretEnforcement,
],
option: LONG_FLAG_CCK,
present: create_opts.keys.cck.is_some(),
}];
for r in rules {
let offenders: Vec<_> = r
.flags
.iter()
.filter(|f| create_opts.flags.contains(f))
.collect();
if !offenders.is_empty() && !r.present {
return Err(clap::Error::raw(
clap::error::ErrorKind::MissingRequiredArgument,
format!(
"flag(s) {} require(s) --{}",
offenders
.iter()
.map(|f| format!("{:?}", f))
.collect::<Vec<_>>()
.join(", "),
r.option
),
));
}
}
// Check for conflicts between --flags and --disable-flags
if !create_opts.flags.is_empty() && !create_opts.disable_flags.is_empty() {
use std::collections::HashSet;
let flags_set: HashSet<_> = create_opts.flags.iter().collect();
let disable_flags_set: HashSet<_> = create_opts.disable_flags.iter().collect();
let conflicts: Vec<_> = flags_set
.intersection(&disable_flags_set)
.copied()
.collect();
if !conflicts.is_empty() {
return Err(clap::Error::raw(
clap::error::ErrorKind::ArgumentConflict,
// Print the flag name using the kebab-case notation (using to_possible_value)
format!(
"Conflicting flags detected: the following flags are specified in both --flags and --disable-flags: {}",
conflicts.iter().map(|x| format!("{x}")).collect::<Vec<_>>().join(", ")
),
));
}
}
// Check for mutually exclusive flags within --flags
let exclusion_rules = [MutuallyExclusiveFlags {
flags: &[
SeHdrFlagName::CckExtensionSecretEnforcement,
SeHdrFlagName::CckUpdate,
],
}];
for rule in exclusion_rules {
let conflicting_flags: Vec<_> = rule
.flags
.iter()
.filter(|f| create_opts.flags.contains(f))
.collect();
if conflicting_flags.len() > 1 {
return Err(clap::Error::raw(
clap::error::ErrorKind::ArgumentConflict,
format!(
"The following flags cannot be used together: {}",
conflicting_flags
.iter()
.map(|f| format!("'{}'", f))
.collect::<Vec<_>>()
.join(", ")
),
));
}
}
Ok(())
}
_ => Ok(()),
@@ -100,6 +257,8 @@ pub struct ComponentPaths {
pub parmfile: Option<PathBuf>,
}
const LONG_FLAG_CCK: &str = "cck";
/// CLI Argument collection for handling user-provided keys.
#[derive(Args, Debug)]
#[cfg_attr(test, derive(Default))]
@@ -109,7 +268,7 @@ pub struct UserKeys {
/// The file must contain exactly 32 bytes of data. In previous versions,
/// this option was called '--comm-key'.
#[arg(
long,
long = LONG_FLAG_CCK,
value_name = "FILE",
group = "cck-available",
visible_alias = "comm-key"
@@ -184,11 +343,13 @@ pub struct CreateBootImageLegacyFlags {
#[arg(long, action = clap::ArgAction::SetTrue, conflicts_with="enable_pckmo", group="header-flags")]
pub disable_pckmo: Option<bool>,
/// Enable the support for the HMAC PCKMO key encryption function.
/// Enable the support for the HMAC PCKMO key encryption function (default for header version
/// 2).
#[arg(long, action = clap::ArgAction::SetTrue, group="header-flags")]
pub enable_pckmo_hmac: Option<bool>,
/// Disable the support for the HMAC PCKMO key encryption function (default).
/// Disable the support for the HMAC PCKMO key encryption function (default for header version
/// 1).
#[arg(long, action = clap::ArgAction::SetTrue, conflicts_with="enable_pckmo_hmac", group="header-flags")]
pub disable_pckmo_hmac: Option<bool>,
@@ -519,7 +680,6 @@ impl GenprotimgCliOptions {
}
#[derive(Parser, Debug)]
#[cfg_attr(test, derive(Default))]
pub struct CreateBootImageArgs {
#[clap(flatten)]
pub component_paths: ComponentPaths,
@@ -542,16 +702,62 @@ pub struct CreateBootImageArgs {
#[arg(long)]
pub overwrite: bool,
/// Specify the Secure Execution header version to use.
#[arg(long = "hdr-version", value_name = "VERSION", default_value_t = HdrVersion::V1)]
pub hdr_version: HdrVersion,
#[clap(flatten)]
pub keys: UserKeys,
#[clap(flatten)]
// TODO Declare as deprecated
pub legacy_flags: CreateBootImageLegacyFlags,
/// Set control flags using comma-separated flag names.
///
/// Specify flags to enable using their names.
#[arg(
long,
value_name = "FLAGS",
value_delimiter = ',',
conflicts_with_all = ["header-flags", "x_pcf", "x_scf"]
)]
pub flags: Vec<SeHdrFlagName>,
/// Set control flags using comma-separated flag names.
///
/// Specify flags to disable using their names.
#[arg(
long,
value_name = "FLAGS",
value_delimiter = ',',
conflicts_with_all = ["header-flags", "x_pcf", "x_scf"]
)]
pub disable_flags: Vec<SeHdrFlagName>,
#[clap(flatten)]
pub experimental_args: CreateBootImageExperimentalArgs,
}
#[cfg(test)]
impl Default for CreateBootImageArgs {
fn default() -> Self {
Self {
component_paths: ComponentPaths::default(),
output: PathBuf::new(),
certificate_args: CertificateOptions::default(),
no_component_check: false,
overwrite: false,
hdr_version: HdrVersion::V1,
keys: UserKeys::default(),
legacy_flags: CreateBootImageLegacyFlags::default(),
flags: Vec::new(),
disable_flags: Vec::new(),
experimental_args: CreateBootImageExperimentalArgs::default(),
}
}
}
/// Experimental options
#[derive(Args, Debug)]
#[cfg_attr(test, derive(Default))]
@@ -666,6 +872,62 @@ mod test {
map
}
// Helper to test valid args - generic over parse and convert functions
fn test_valid_args<F>(args_list: Vec<Vec<&str>>, parse_and_convert: F, name: &str)
where
F: Fn(&[&str]) -> Result<CliOptions, clap::Error>,
{
for arg in args_list {
let res = parse_and_convert(&arg);
#[allow(clippy::use_debug, clippy::print_stdout)]
if let Err(e) = &res {
println!("{name} arg: {arg:?}");
println!("{e}");
}
assert!(res.is_ok(), "{name} failed for: {arg:?}");
}
}
// Helper to test invalid args - expects parse or validation to fail
fn test_invalid_args<F>(
test_cases: &[(&[Vec<String>], clap::error::ErrorKind, &str)],
cmd_prefix: &[&str],
parse_and_convert: F,
name: &str,
) where
F: Fn(&[&str]) -> Result<CliOptions, clap::Error>,
{
for (test_group, expected_kind, kind_name) in test_cases {
for args in *test_group {
let full_args = [
cmd_prefix.to_vec(),
Vec::from_iter(args.iter().map(String::as_str)),
]
.concat();
// Try parse and convert
let parse_result = parse_and_convert(&full_args);
let err = match parse_result {
Ok(cli_opts) => {
// Parse succeeded, validation must fail
validate_cli(&cli_opts).expect_err(&format!(
"{name}: Expected error ({kind_name}) but both parse and validation succeeded for: {full_args:?}"
))
}
Err(e) => e, // Parse failed as expected
};
assert_eq!(
err.kind(),
*expected_kind,
"{name}: Expected {kind_name} but got {:?} for args: {full_args:?}\nError: {err}",
err.kind()
);
}
}
}
#[test]
#[rustfmt::skip]
fn genprotimg_and_pvimg_create_args() {
@@ -734,16 +996,67 @@ mod test {
// --comm-key with --enable-cck-update (alias test)
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("comm-key", ["--comm-key", "/dev/null"]),
CliOption::new("enable-cck-update", ["--enable-cck-update"])])),
// --disable-flags tests
// Test --disable-flags alone
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("disable-flags", ["--disable-flags", &SeHdrFlagName::PckmoHmac.to_string()])])),
// Test --flags and --disable-flags without conflict
flat_map_collect(insert(mvca.clone(), vec![
CliOption::new("flags", ["--flags", &SeHdrFlagName::ConfidentialDump.to_string()]),
CliOption::new("disable-flags", ["--disable-flags", &SeHdrFlagName::PckmoHmac.to_string()]),
CliOption::new("cck", ["--cck", "/dev/null"])
])),
// Test multiple --disable-flags
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("disable-flags", ["--disable-flags", &format!("{},{}", SeHdrFlagName::PckmoHmac, SeHdrFlagName::BackupTargetKeys)])])),
// Test --flags and --disable-flags with different flags
flat_map_collect(insert(mvca.clone(), vec![
CliOption::new("flags", ["--flags", &format!("{},{}", SeHdrFlagName::ConfidentialDump, SeHdrFlagName::BackupTargetKeys)]),
CliOption::new("disable-flags", ["--disable-flags", &format!("{},{}", SeHdrFlagName::PckmoHmac, SeHdrFlagName::NoComponentEncryption)]),
CliOption::new("cck", ["--cck", "/dev/null"])
])),
// --flags tests (equivalent to --enable-* tests)
// Test --flags with confidential-dump (equivalent to --enable-dump)
flat_map_collect(insert(mvca.clone(), vec![
CliOption::new("flags", ["--flags", &SeHdrFlagName::ConfidentialDump.to_string()]),
CliOption::new("cck", ["--cck", "/dev/null"])
])),
// Test with --overwrite
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("overwrite", ["--overwrite"])])),
// Test with --no-component-check
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("no-component-check", ["--no-component-check"])])),
// Test with all PCKMO flags
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("flags", ["--flags", &format!("{},{},{},{}",
SeHdrFlagName::PckmoDeaTdea, SeHdrFlagName::PckmoAes, SeHdrFlagName::PckmoEcc, SeHdrFlagName::PckmoHmac)])])),
// Test with NoComponentEncryption flag
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("flags", ["--flags", &SeHdrFlagName::NoComponentEncryption.to_string()])])),
// Test with HdrVersion V2
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("hdr-version", ["--hdr-version", "2"])])),
];
let invalid_create_args = [
// Invalid test cases grouped by expected error kind
let invalid_missing_required = [
flat_map_collect(remove(mvcanv.clone(), "no-verify")),
flat_map_collect(remove(mvcanv.clone(), "image")),
flat_map_collect(remove(mvcanv.clone(), "hkd")),
flat_map_collect(remove(mvcanv, "output")),
// missing both `--cck' and `--enable-cck-update'
// missing both `--cck' and `--enable-cck-update' (required by --enable-dump)
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-dump", ["--enable-dump"])])),
// Test --flags with confidential-dump but missing --cck (equivalent to --enable-dump without --cck)
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("flags", ["--flags", &SeHdrFlagName::ConfidentialDump.to_string()])])),
// --enable-cck-extension-secret without cck-available (validation error, semantically missing required)
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-cck-extension-secret", ["--enable-cck-extension-secret"])])),
];
let invalid_value = [
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("x-header-key", ["--hdr-key"]),])),
];
let invalid_conflict = [
// -v and -q cannot be combined
flat_map_collect(insert(mvca.clone(), vec![
CliOption::new("verbose", ["-v"]),
@@ -761,7 +1074,6 @@ mod test {
CliOption::new("disable-pckmo", ["--disable-pckmo"])])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-image-encryption", ["--enable-image-encryption"]),
CliOption::new("disable-image-encryption", ["--disable-image-encryption"])])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("x-header-key", ["--hdr-key"]),])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("extension", ["--enable-cck-extension-secret"]),
CliOption::new("update", ["--enable-cck-update"])])),
@@ -776,13 +1088,55 @@ mod test {
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("disable-image-encryption", ["--disable-image-encryption"]),
CliOption::new("image-key", ["--image-key", "/dev/null"])])),
// cck-available group tests: invalid combinations
// --enable-cck-extension-secret without cck-available
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-cck-extension-secret", ["--enable-cck-extension-secret"])])),
// --enable-cck-extension-secret with --enable-cck-update and --cck (conflict takes precedence)
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-cck-extension-secret", ["--enable-cck-extension-secret"]),
CliOption::new("enable-cck-update", ["--enable-cck-update"]),
CliOption::new("cck", ["--cck", "/dev/null"])])),
// --disable-flags conflict tests
// Test conflict between --flags and --disable-flags (same flag in both)
flat_map_collect(insert(mvca.clone(), vec![
CliOption::new("flags", ["--flags", &SeHdrFlagName::ConfidentialDump.to_string()]),
CliOption::new("disable-flags", ["--disable-flags", &SeHdrFlagName::ConfidentialDump.to_string()]),
CliOption::new("cck", ["--cck", "/dev/null"])
])),
// Test multiple conflicts
flat_map_collect(insert(mvca.clone(), vec![
CliOption::new("flags", ["--flags", &format!("{},{}", SeHdrFlagName::ConfidentialDump, SeHdrFlagName::PckmoHmac)]),
CliOption::new("disable-flags", ["--disable-flags", &format!("{},{}", SeHdrFlagName::ConfidentialDump, SeHdrFlagName::PckmoHmac)]),
CliOption::new("cck", ["--cck", "/dev/null"])
])),
// --flags conflicts with --x-pcf
flat_map_collect(insert(mvca.clone(), vec![
CliOption::new("flags", ["--flags", &SeHdrFlagName::ConfidentialDump.to_string()]),
CliOption::new("x-pcf", ["--x-pcf", "0x0"]),
CliOption::new("cck", ["--cck", "/dev/null"])
])),
// --flags conflicts with --x-scf
flat_map_collect(insert(mvca.clone(), vec![
CliOption::new("flags", ["--flags", &SeHdrFlagName::ConfidentialDump.to_string()]),
CliOption::new("x-scf", ["--x-scf", "0x0"]),
CliOption::new("cck", ["--cck", "/dev/null"])
])),
// --disable-flags conflicts with --x-pcf
flat_map_collect(insert(mvca.clone(), vec![
CliOption::new("disable-flags", ["--disable-flags", &SeHdrFlagName::PckmoHmac.to_string()]),
CliOption::new("x-pcf", ["--x-pcf", "0x0"])
])),
// --disable-flags conflicts with --x-scf
flat_map_collect(insert(mvca.clone(), vec![
CliOption::new("disable-flags", ["--disable-flags", &SeHdrFlagName::PckmoHmac.to_string()]),
CliOption::new("x-scf", ["--x-scf", "0x0"])
])),
// Test --flags with cck-extension-secret and cck-update and --cck
// (conflict, equivalent to --enable-cck-extension-secret)
flat_map_collect(insert(mvca, vec![
CliOption::new("flags-cck-extension-secret", ["--flags", &SeHdrFlagName::CckExtensionSecretEnforcement.to_string()]),
CliOption::new("flags-enable-cck-update", ["--flags", &SeHdrFlagName::CckUpdate.to_string()]),
CliOption::new("cck", ["--cck", "/dev/null"])
])),
];
let mut genprotimg_valid_args = vec![
@@ -794,53 +1148,31 @@ mod test {
vec!["pvimg", "version"],
];
// Test for invalid combinations
let mut genprotimg_invalid_args = vec![
vec!["genprotimg"],
];
let mut pvimg_invalid_args = vec![
vec!["pvimg"],
];
// Test that `genprotimg` and `pvimg create` behave equally.
for create_args in &valid_create_args {
genprotimg_valid_args.push([["genprotimg"].to_vec(), Vec::from_iter(create_args.iter().map(String::as_str))].concat());
pvimg_valid_args.push([["pvimg", "create"].to_vec(), Vec::from_iter(create_args.iter().map(String::as_str))].concat());
}
for invalid_create_args in &invalid_create_args {
genprotimg_invalid_args.push([["genprotimg"].to_vec(), Vec::from_iter(invalid_create_args.iter().map(String::as_str))].concat());
pvimg_invalid_args.push([["pvimg", "create"].to_vec(), Vec::from_iter(invalid_create_args.iter().map(String::as_str))].concat());
}
// Test invalid args with expected error kinds
let test_cases = [
(&invalid_missing_required[..], clap::error::ErrorKind::MissingRequiredArgument, "MissingRequiredArgument"),
(&invalid_value[..], clap::error::ErrorKind::InvalidValue, "InvalidValue"),
(&invalid_conflict[..], clap::error::ErrorKind::ArgumentConflict, "ArgumentConflict"),
];
for arg in pvimg_valid_args {
let res = CliOptions::try_parse_from(&arg);
#[allow(clippy::use_debug, clippy::print_stdout)]
if let Err(e) = &res {
println!("arg: {arg:?}");
println!("{e}");
}
assert!(res.is_ok());
}
// Parse and convert functions for each CLI variant
let parse_pvimg = |args: &[&str]| CliOptions::try_parse_from(args);
// The into converts Result<GenprotimgCliOptions, Error> into Result<CliOptions, Error>
let parse_genprotimg = |args: &[&str]| GenprotimgCliOptions::try_parse_from(args).map(Into::into);
for arg in pvimg_invalid_args {
let res = CliOptions::try_parse_from(&arg);
assert!(res.is_err());
}
for arg in genprotimg_valid_args {
let res = GenprotimgCliOptions::try_parse_from(&arg);
#[allow(clippy::use_debug, clippy::print_stdout)]
if let Err(e) = &res {
println!("arg: {arg:?}");
println!("{e}");
}
assert!(res.is_ok());
}
for arg in genprotimg_invalid_args {
let res = GenprotimgCliOptions::try_parse_from(&arg);
assert!(res.is_err());
// Test both CLI variants
for (name, valid_args, cmd_prefix, parse_fn) in [
("pvimg", pvimg_valid_args, &["pvimg", "create"] as &[&str], &parse_pvimg as &dyn Fn(&[&str]) -> Result<CliOptions, clap::Error>),
("genprotimg", genprotimg_valid_args, &["genprotimg"] as &[&str], &parse_genprotimg as &dyn Fn(&[&str]) -> Result<CliOptions, clap::Error>),
] {
test_valid_args(valid_args, parse_fn, name);
test_invalid_args(&test_cases, cmd_prefix, parse_fn, name);
}
}
@@ -888,13 +1220,37 @@ mod test {
CliOption::new("verbose", ["-VVV"]),
],
)),
// Test with --host-key-document
flat_map_collect(insert(
args.clone(),
vec![
CliOption::new("host-key-document", ["--host-key-document", "/dev/null"]),
CliOption::new("image", ["/dev/null"]),
],
)),
// Test with multiple --host-key-document (comma-separated)
flat_map_collect(insert(
args.clone(),
vec![
CliOption::new(
"host-key-document",
["--host-key-document", "/dev/null,/dev/zero"],
),
CliOption::new("image", ["/dev/null"]),
],
)),
];
let invalid_test_args = [
// Invalid test cases grouped by expected error kind
let invalid_missing_required = [
// Missing required test-args group (only image provided, no host-key info)
flat_map_collect(insert(
args.clone(),
vec![CliOption::new("image", ["/dev/null"])],
)),
];
let invalid_conflict = [
// the argument '--key-hashes[=<FILE>]' cannot be used with '--host-key-document
// <FILE>'
flat_map_collect(insert(
@@ -905,20 +1261,17 @@ mod test {
CliOption::new("image", ["/dev/null"]),
],
)),
flat_map_collect(insert(
args,
vec![
CliOption::new("host-key-hashes2", ["--key-hashes", "/sys/null"]),
CliOption::new("image", ["--", "/dev/null"]),
],
)),
];
let mut pvimg_valid_args = vec![];
let invalid_unknown_arg = [flat_map_collect(insert(
args.clone(),
vec![
CliOption::new("host-key-hashes2", ["--key-hashes", "/sys/null"]),
CliOption::new("image", ["--", "/dev/null"]),
],
))];
// Test for invalid combinations
// Input is missing
let mut pvimg_invalid_args = vec![vec!["pvimg", "test"]];
let mut pvimg_valid_args = vec![];
for create_args in &valid_test_args {
pvimg_valid_args.push(
@@ -930,30 +1283,36 @@ mod test {
);
}
for invalid_test_arg in &invalid_test_args {
pvimg_invalid_args.push(
[
["pvimg", "test"].to_vec(),
Vec::from_iter(invalid_test_arg.iter().map(String::as_str)),
]
.concat(),
);
}
let test_cases = [
(
&invalid_missing_required[..],
clap::error::ErrorKind::MissingRequiredArgument,
"MissingRequiredArgument",
),
(
&invalid_conflict[..],
clap::error::ErrorKind::ArgumentConflict,
"ArgumentConflict",
),
(
&invalid_unknown_arg[..],
clap::error::ErrorKind::UnknownArgument,
"UnknownArgument",
),
];
for arg in pvimg_valid_args {
let res = CliOptions::try_parse_from(&arg);
#[allow(clippy::use_debug, clippy::print_stdout)]
if let Err(e) = &res {
println!("arg: {arg:?}");
println!("{e}");
}
assert!(res.is_ok());
}
for arg in pvimg_invalid_args {
let res = CliOptions::try_parse_from(&arg);
assert!(res.is_err());
}
let parse_pvimg = |args: &[&str]| CliOptions::try_parse_from(args);
test_valid_args(
pvimg_valid_args,
&parse_pvimg as &dyn Fn(&[&str]) -> Result<CliOptions, clap::Error>,
"pvimg test",
);
test_invalid_args(
&test_cases,
&["pvimg", "test"],
&parse_pvimg as &dyn Fn(&[&str]) -> Result<CliOptions, clap::Error>,
"pvimg test",
);
}
#[test]
@@ -1044,9 +1403,27 @@ mod test {
["--print-schema", "json"],
)],
)),
// --show-secrets with --hdr-key
flat_map_collect(insert(
args.clone(),
vec![
CliOption::new("hdr-key", ["--hdr-key", "/dev/null"]),
CliOption::new("show-secrets", ["--show-secrets"]),
CliOption::new("image", ["/dev/null"]),
],
)),
// text:full format
flat_map_collect(insert(
args.clone(),
vec![
CliOption::new("format", ["--format", "text:full"]),
CliOption::new("image", ["/dev/null"]),
],
)),
];
let invalid_test_args = [
// Invalid test cases grouped by expected error kind
let invalid_value = [
// No default defined for --format
flat_map_collect(insert(
args.clone(),
@@ -1055,6 +1432,9 @@ mod test {
CliOption::new("image", ["--", "/dev/null"]),
],
)),
];
let invalid_conflict = [
// --print-json-schema conflicts with input
flat_map_collect(insert(
args.clone(),
@@ -1081,7 +1461,7 @@ mod test {
)),
// --print-json-schema conflicts with --show-secrets
flat_map_collect(insert(
args,
args.clone(),
vec![
CliOption::new("print-json-schema", ["--print-schema", "json"]),
CliOption::new("show-secrets", ["--show-secrets"]),
@@ -1091,10 +1471,6 @@ mod test {
let mut pvimg_valid_args = vec![];
// Test for invalid combinations
// Input is missing
let mut pvimg_invalid_args = vec![vec!["pvimg", "info"]];
for create_args in &valid_test_args {
pvimg_valid_args.push(
[
@@ -1105,30 +1481,126 @@ mod test {
);
}
for invalid_test_arg in &invalid_test_args {
pvimg_invalid_args.push(
[
["pvimg", "info"].to_vec(),
Vec::from_iter(invalid_test_arg.iter().map(String::as_str)),
]
.concat(),
);
}
let invalid_format = [
// Invalid format variant for text
flat_map_collect(insert(
args.clone(),
vec![
CliOption::new("format", ["--format", "text:minify"]),
CliOption::new("image", ["/dev/null"]),
],
)),
// Invalid format variant for json
flat_map_collect(insert(
args.clone(),
vec![
CliOption::new("format", ["--format", "json:full"]),
CliOption::new("image", ["/dev/null"]),
],
)),
];
for arg in pvimg_valid_args {
let res = CliOptions::try_parse_from(&arg);
#[allow(clippy::use_debug, clippy::print_stdout)]
if let Err(e) = &res {
println!("arg: {arg:?}");
println!("{e}");
}
assert!(res.is_ok());
}
let test_cases = [
(
&invalid_value[..],
clap::error::ErrorKind::InvalidValue,
"InvalidValue",
),
(
&invalid_conflict[..],
clap::error::ErrorKind::ArgumentConflict,
"ArgumentConflict",
),
(
&invalid_format[..],
clap::error::ErrorKind::ValueValidation,
"ValueValidation",
),
];
for arg in pvimg_invalid_args {
let res = CliOptions::try_parse_from(&arg);
assert!(res.is_err());
}
let parse_pvimg = |args: &[&str]| CliOptions::try_parse_from(args);
test_valid_args(
pvimg_valid_args,
&parse_pvimg as &dyn Fn(&[&str]) -> Result<CliOptions, clap::Error>,
"pvimg info",
);
test_invalid_args(
&test_cases,
&["pvimg", "info"],
&parse_pvimg as &dyn Fn(&[&str]) -> Result<CliOptions, clap::Error>,
"pvimg info",
);
}
#[test]
fn test_hdr_version_conversions() {
assert_eq!(HkdVersion::from(HdrVersion::V1), HkdVersion::Classical);
assert_eq!(HkdVersion::from(HdrVersion::V2), HkdVersion::Hybrid);
}
#[test]
fn test_output_format_kind_display() {
assert_eq!(OutputFormatKind::Text.to_string(), "human-readable");
assert_eq!(OutputFormatKind::Json.to_string(), "JSON");
}
#[test]
fn test_output_format_kind_from_str() {
assert_eq!(
"text".parse::<OutputFormatKind>().unwrap(),
OutputFormatKind::Text
);
assert_eq!(
"json".parse::<OutputFormatKind>().unwrap(),
OutputFormatKind::Json
);
assert!("invalid".parse::<OutputFormatKind>().is_err());
}
#[test]
fn test_new_version_cmd_opts() {
let opts = CliOptions::new_version_cmd_opts();
assert!(opts.version);
assert!(matches!(opts.cmd, SubCommands::Version));
}
#[test]
fn test_genprotimg_to_cli_options_conversion() {
let genprotimg_opts = GenprotimgCliOptions {
args: Box::new(CreateBootImageArgs::default()),
verbose: DeprecatedVerbosityOptions::default(),
version: (),
help_all: (),
help_experimental: (),
};
let cli_opts: CliOptions = genprotimg_opts.into();
assert!(!cli_opts.version);
assert!(matches!(cli_opts.cmd, SubCommands::Create(_)));
}
#[test]
fn test_se_hdr_flag_name_display() {
assert_eq!(
SeHdrFlagName::ConfidentialDump.to_string(),
"confidential-dump"
);
assert_eq!(SeHdrFlagName::PckmoDeaTdea.to_string(), "pckmo-dea-tdea");
assert_eq!(SeHdrFlagName::PckmoAes.to_string(), "pckmo-aes");
assert_eq!(SeHdrFlagName::PckmoEcc.to_string(), "pckmo-ecc");
assert_eq!(SeHdrFlagName::PckmoHmac.to_string(), "pckmo-hmac");
assert_eq!(
SeHdrFlagName::BackupTargetKeys.to_string(),
"backup-target-keys"
);
assert_eq!(
SeHdrFlagName::CckExtensionSecretEnforcement.to_string(),
"cck-extension-secret-enforcement"
);
assert_eq!(SeHdrFlagName::CckUpdate.to_string(), "cck-update");
assert_eq!(
SeHdrFlagName::NoComponentEncryption.to_string(),
"no-component-encryption"
);
}
#[test]

View File

@@ -8,7 +8,7 @@ mod info;
mod test;
mod version;
pub const CMD_FN: &[&str] = &["+create", "+test", "+info"];
pub const CMD_FN: &[&str] = &["+create", "+test", "+info", "+quantumsafe"];
pub use create::create;
pub use info::info;

View File

@@ -5,18 +5,21 @@
use std::fs::OpenOptions;
use std::io::BufReader;
use anyhow::{Context, Result};
use log::{debug, warn};
use anyhow::{anyhow, Context, Result};
use log::{debug, info, warn};
use pv::misc::{open_file, try_parse_u64};
use pv::request::HostKey;
use pvimg::error::OwnExitCode;
use pvimg::secured_comp::ComponentTrait;
use pvimg::uvdata::{
ControlFlagTrait, ControlFlagsTrait, FlagData, PcfV1, PlaintextControlFlagsV1, ScfV1,
SeHdrDataV1, SecretControlFlagsV1,
EffectiveControlFlags, FlagState, FlagsOverride, SeHdrControlFlags, SeHdrControlFlagsModel,
SeHdrDataV1, SeHdrDataV2, SeHdrFlag, SeHdrVersion, SeTarget,
};
use utils::{AtomicFile, AtomicFileOperation};
use crate::cli::{ComponentPaths, CreateBootImageArgs};
use crate::cli::{
ComponentPaths, CreateBootImageArgs, HdrVersion, HdrVersionSelection, SeHdrFlagName,
};
use crate::cmd::common::read_user_provided_keys;
use crate::se_img::{SeHdrArgs, SeImgBuilder};
use crate::se_img_comps::cmdline::Cmdline;
@@ -24,6 +27,21 @@ use crate::se_img_comps::kernel::S390Kernel;
use crate::se_img_comps::ramdisk::Ramdisk;
use crate::se_img_comps::{check_components, Component};
/// Convert CLI flag name to internal SeHdrFlag
fn convert_flag_name(name: SeHdrFlagName) -> SeHdrFlag {
match name {
SeHdrFlagName::ConfidentialDump => SeHdrFlag::ConfidentialDump,
SeHdrFlagName::PckmoDeaTdea => SeHdrFlag::PckmoDeaTdea,
SeHdrFlagName::PckmoAes => SeHdrFlag::PckmoAes,
SeHdrFlagName::PckmoEcc => SeHdrFlag::PckmoEcc,
SeHdrFlagName::PckmoHmac => SeHdrFlag::PckmoHmac,
SeHdrFlagName::BackupTargetKeys => SeHdrFlag::BackupTargetKeys,
SeHdrFlagName::CckExtensionSecretEnforcement => SeHdrFlag::CckExtensionSecretEnforcement,
SeHdrFlagName::CckUpdate => SeHdrFlag::CckUpdate,
SeHdrFlagName::NoComponentEncryption => SeHdrFlag::NoComponentEncryption,
}
}
/// The returned vector is sorted by the occurrence in the memory layout:
/// First the kernel, then the ramdisk and then the kernel cmdline.
///
@@ -43,91 +61,272 @@ fn components(component_args: &ComponentPaths) -> Result<Vec<Component>> {
Ok(components)
}
fn parse_flags(
args: &CreateBootImageArgs,
) -> Result<(PlaintextControlFlagsV1, SecretControlFlagsV1)> {
let lf = &args.legacy_flags;
/// Parse new-style flags (--flags and --disable-flags)
fn parse_new_style_flags(
flags: &[SeHdrFlagName],
disable_flags: &[SeHdrFlagName],
version: SeHdrVersion,
) -> Result<(FlagsOverride<SeHdrFlag>, FlagsOverride<SeHdrFlag>)> {
let target = SeTarget::from_se_hdr_version(version);
let mut pcf_overrides: FlagsOverride<SeHdrFlag> = FlagsOverride::new();
let mut scf_overrides: FlagsOverride<SeHdrFlag> = FlagsOverride::new();
if flags.is_empty() && disable_flags.is_empty() {
return Ok((pcf_overrides, scf_overrides));
}
let pcf_model = SeHdrControlFlagsModel::pcf_for_target(target);
let scf_model = SeHdrControlFlagsModel::scf_for_target(target);
let pcf_supported = pcf_model.supported_flags();
let scf_supported = scf_model.supported_flags();
// Helper function to process flags
let mut process_flags =
|flag_list: &[SeHdrFlagName], enable: bool, flag_type: &str| -> Result<()> {
if flag_list.is_empty() {
return Ok(());
}
let converted_flags: Vec<SeHdrFlag> =
flag_list.iter().map(|&f| convert_flag_name(f)).collect();
for flag in &converted_flags {
if pcf_supported.contains(flag) {
if enable {
pcf_overrides.enable(*flag);
} else {
pcf_overrides.disable(*flag);
}
}
if scf_supported.contains(flag) {
if enable {
scf_overrides.enable(*flag);
} else {
scf_overrides.disable(*flag);
}
}
}
// Check if all flags were consumed (supported by either pcf or scf)
let unsupported_flags: Vec<&SeHdrFlag> = converted_flags
.iter()
.filter(|flag| !pcf_supported.contains(flag) && !scf_supported.contains(flag))
.collect();
if !unsupported_flags.is_empty() {
return Err(anyhow!(
"The following {} are not supported for SE header version {:?}: {:?}",
flag_type,
version,
unsupported_flags
));
}
Ok(())
};
process_flags(flags, true, "flags")?;
process_flags(disable_flags, false, "disable flags")?;
Ok((pcf_overrides, scf_overrides))
}
/// Parse legacy-style flags
fn parse_legacy_flags(
legacy_flags: &crate::cli::CreateBootImageLegacyFlags,
) -> (FlagsOverride<SeHdrFlag>, FlagsOverride<SeHdrFlag>) {
macro_rules! flag_disabled {
($cli_flag:expr, $control_flags:expr) => {
$cli_flag
.filter(|x| *x)
.and(Some(ControlFlagTrait::all_disabled($control_flags)))
$cli_flag.filter(|x| *x).map(|_| {
let mut flags = FlagsOverride::new();
flags.disable_all($control_flags);
flags
})
};
}
macro_rules! flag_enabled {
($cli_flag:expr, $control_flags:expr) => {
$cli_flag
.filter(|x| *x)
.and(Some(ControlFlagTrait::all_enabled($control_flags)))
$cli_flag.filter(|x| *x).map(|_| {
let mut flags = FlagsOverride::new();
flags.enable_all($control_flags);
flags
})
};
}
let plaintext_flags: Vec<FlagData<PcfV1>> = [
flag_disabled!(lf.disable_dump, [PcfV1::AllowDumping]),
flag_enabled!(lf.enable_dump, [PcfV1::AllowDumping]),
flag_disabled!(lf.disable_pckmo, PlaintextControlFlagsV1::PCKMO),
flag_enabled!(lf.enable_pckmo, PlaintextControlFlagsV1::PCKMO),
flag_disabled!(lf.disable_pckmo_hmac, [PcfV1::PckmoHmac]),
flag_enabled!(lf.enable_pckmo_hmac, [PcfV1::PckmoHmac]),
flag_disabled!(lf.disable_backup_keys, [PcfV1::BackupTargetKeys]),
flag_enabled!(lf.enable_backup_keys, [PcfV1::BackupTargetKeys]),
flag_enabled!(lf.disable_image_encryption, [PcfV1::NoComponentEncryption]),
flag_disabled!(lf.enable_image_encryption, [PcfV1::NoComponentEncryption]),
]
.into_iter()
.flatten()
.flatten()
.collect();
// This is ensured by Clap's `conflicts_with`.
assert!(PlaintextControlFlagsV1::no_duplicates(&plaintext_flags));
let secret_flags: Vec<FlagData<ScfV1>> = [
let pcf_overrides = [
flag_disabled!(legacy_flags.disable_dump, [SeHdrFlag::ConfidentialDump]),
flag_enabled!(legacy_flags.enable_dump, [SeHdrFlag::ConfidentialDump]),
flag_disabled!(legacy_flags.disable_pckmo, SeHdrControlFlagsModel::PCKMO),
flag_enabled!(legacy_flags.enable_pckmo, SeHdrControlFlagsModel::PCKMO),
flag_disabled!(legacy_flags.disable_pckmo_hmac, [SeHdrFlag::PckmoHmac]),
flag_enabled!(legacy_flags.enable_pckmo_hmac, [SeHdrFlag::PckmoHmac]),
flag_disabled!(
lf.disable_cck_extension_secret,
[ScfV1::CckExtensionSecretEnforcement]
legacy_flags.disable_backup_keys,
[SeHdrFlag::BackupTargetKeys]
),
flag_enabled!(
lf.enable_cck_extension_secret,
[ScfV1::CckExtensionSecretEnforcement]
legacy_flags.enable_backup_keys,
[SeHdrFlag::BackupTargetKeys]
),
flag_enabled!(
legacy_flags.disable_image_encryption,
[SeHdrFlag::NoComponentEncryption]
),
flag_disabled!(
legacy_flags.enable_image_encryption,
[SeHdrFlag::NoComponentEncryption]
),
flag_disabled!(lf.disable_cck_update, [ScfV1::CckUpdateAllowed]),
flag_enabled!(lf.enable_cck_update, [ScfV1::CckUpdateAllowed]),
]
.into_iter()
.flatten()
.fold(FlagsOverride::new(), |mut acc, override_set| {
for (flag, state) in override_set.iter() {
match state {
FlagState::Enabled => acc.enable(*flag),
FlagState::Disabled => acc.disable(*flag),
}
}
acc
});
let scf_overrides = [
flag_disabled!(
legacy_flags.disable_cck_extension_secret,
[SeHdrFlag::CckExtensionSecretEnforcement]
),
flag_enabled!(
legacy_flags.enable_cck_extension_secret,
[SeHdrFlag::CckExtensionSecretEnforcement]
),
flag_disabled!(legacy_flags.disable_cck_update, [SeHdrFlag::CckUpdate]),
flag_enabled!(legacy_flags.enable_cck_update, [SeHdrFlag::CckUpdate]),
]
.into_iter()
.flatten()
.collect();
// This is ensured by Clap's `conflicts_with`.
assert!(SecretControlFlagsV1::no_duplicates(&secret_flags));
.fold(FlagsOverride::new(), |mut acc, override_set| {
for (flag, state) in override_set.iter() {
match state {
FlagState::Enabled => acc.enable(*flag),
FlagState::Disabled => acc.disable(*flag),
}
}
acc
});
let mut pcf: PlaintextControlFlagsV1 = match &args.experimental_args.x_pcf {
Some(v) => try_parse_u64(v, "x-pcf")?.into(),
None => PlaintextControlFlagsV1::default(),
};
pcf.parse_flags(&plaintext_flags);
debug!("Using plaintext flags: {pcf:#x}");
(pcf_overrides, scf_overrides)
}
let mut scf: SecretControlFlagsV1 = match &args.experimental_args.x_scf {
Some(v) => try_parse_u64(v, "x-scf")?.into(),
None => SecretControlFlagsV1::default(),
/// Apply experimental overrides to control flags
fn apply_experimental_overrides(
pcf_overrides: &FlagsOverride<SeHdrFlag>,
scf_overrides: &FlagsOverride<SeHdrFlag>,
x_pcf: &Option<String>,
x_scf: &Option<String>,
target: SeTarget,
) -> Result<(
EffectiveControlFlags<SeHdrFlag>,
EffectiveControlFlags<SeHdrFlag>,
)> {
let pcf = match x_pcf {
Some(v) => {
assert_eq!(pcf_overrides.len(), 0);
SeHdrControlFlags::from_u64(try_parse_u64(v, "x-pcf")?, target, true)
}
None => SeHdrControlFlagsModel::pcf_for_target(target).with_overrides(pcf_overrides)?,
};
let scf = match x_scf {
Some(v) => {
assert_eq!(scf_overrides.len(), 0);
SeHdrControlFlags::from_u64(try_parse_u64(v, "x-scf")?, target, false)
}
None => SeHdrControlFlagsModel::scf_for_target(target).with_overrides(scf_overrides)?,
};
scf.parse_flags(&secret_flags);
debug!("Using secret flags: {scf:#x}");
Ok((pcf, scf))
}
fn parse_flags(
args: &CreateBootImageArgs,
version: SeHdrVersion,
) -> Result<(
EffectiveControlFlags<SeHdrFlag>,
EffectiveControlFlags<SeHdrFlag>,
)> {
let target = SeTarget::from_se_hdr_version(version);
// Legacy flags and --(disable-)flags are mutually exclusive. Clap semantics
// is used for that.
let (pcf_overrides, scf_overrides) = if args.flags.is_empty() && args.disable_flags.is_empty() {
parse_legacy_flags(&args.legacy_flags)
} else {
parse_new_style_flags(&args.flags, &args.disable_flags, version)?
};
let (pcf, scf) = apply_experimental_overrides(
&pcf_overrides,
&scf_overrides,
&args.experimental_args.x_pcf,
&args.experimental_args.x_scf,
target,
)?;
info!("Using plaintext flags:\n{pcf}");
info!("Using secret flags:\n{scf}");
Ok((pcf, scf))
}
/// Auto-detect the SE header version based on the host keys.
///
/// Returns V2 if any host key is a hybrid key, otherwise returns V1.
fn auto_detect_version(host_keys: &[HostKey]) -> SeHdrVersion {
let use_hybrid_keys = host_keys.iter().any(|k: &HostKey| k.is_hybrid());
if use_hybrid_keys {
SeHdrVersion::V2
} else {
SeHdrVersion::V1
}
}
impl From<HdrVersion> for SeHdrVersion {
fn from(value: HdrVersion) -> Self {
match value {
HdrVersion::V1 => Self::V1,
HdrVersion::V2 => Self::V2,
}
}
}
/// Determine the SE header version to use.
///
/// If an explicit version is provided via CLI, use that.
/// Otherwise, auto-detect based on the host key types.
fn determine_version(cli_version: HdrVersionSelection, host_keys: &[HostKey]) -> SeHdrVersion {
match cli_version {
HdrVersionSelection::Auto => auto_detect_version(host_keys),
HdrVersionSelection::Explicit(hdr_version) => hdr_version.into(),
}
}
/// Create a Secure Execution boot image
pub fn create(opt: &CreateBootImageArgs) -> Result<OwnExitCode> {
// Verify host key documents first, because if they are not valid there is
// no reason to continue.
let verified_host_keys = opt
.certificate_args
.get_verified_hkds("Secure Execution image")?;
let user_provided_keys = read_user_provided_keys(&opt.keys)?;
let (plaintext_flags, secret_flags) = parse_flags(opt)?;
let verified_host_keys = opt.certificate_args.get_verified_hkds(
"Secure Execution image",
HdrVersionSelection::Explicit(opt.hdr_version).map(|v| v.into()),
)?;
if plaintext_flags.is_set(PcfV1::NoComponentEncryption) {
let version = determine_version(
HdrVersionSelection::Explicit(opt.hdr_version),
&verified_host_keys,
);
let user_provided_keys = read_user_provided_keys(&opt.keys)?;
let (plaintext_flags, secret_flags) = parse_flags(opt, version)?;
if plaintext_flags.has(SeHdrFlag::NoComponentEncryption) {
warn!("The components encryption is disabled, make sure that the components do not contain any confidential content.");
}
@@ -140,11 +339,15 @@ pub fn create(opt: &CreateBootImageArgs) -> Result<OwnExitCode> {
// FIXME get rid of the legacy mode. But that's only possible as soon as all
// available tools are updated.
let expected_se_hdr_size = SeHdrDataV1::expected_size(verified_host_keys.len())?;
let expected_se_hdr_size = match version {
SeHdrVersion::V1 => SeHdrDataV1::expected_size(verified_host_keys.len())?,
SeHdrVersion::V2 => SeHdrDataV2::expected_size(verified_host_keys.len())?,
_ => return Err(anyhow!("Unsupported SE header version: {:?}", version)),
};
let mut writer = AtomicFile::with_extension(&opt.output, "part", &mut OpenOptions::new())?;
let mut seimg_ctx = SeImgBuilder::new_v1(
let mut seimg_ctx = SeImgBuilder::new(
&mut writer,
plaintext_flags.is_unset(PcfV1::NoComponentEncryption),
!plaintext_flags.has(SeHdrFlag::NoComponentEncryption),
Some(expected_se_hdr_size),
opt.experimental_args.x_bootloader_directory.as_ref(),
)?;
@@ -218,13 +421,135 @@ mod test {
},
..Default::default()
};
let parsed_flags = super::parse_flags(&args).expect("Failed to parse flags {args:?}");
let mut exp_pcf = Vec::from(PlaintextControlFlagsV1::PCKMO);
exp_pcf.push(PcfV1::AllowDumping);
let pcf = PlaintextControlFlagsV1::from_flags(PcfV1::all_enabled(exp_pcf));
assert_eq!(parsed_flags.0, pcf);
let exp_scf = vec![ScfV1::CckUpdateAllowed];
let scf = SecretControlFlagsV1::from_flags(ScfV1::all_enabled(exp_scf));
assert_eq!(parsed_flags.1, scf);
let parsed_flags =
super::parse_flags(&args, SeHdrVersion::V1).expect("Failed to parse flags {args:?}");
// Build expected PCF
let mut exp_pcf = Vec::from(SeHdrControlFlagsModel::PCKMO);
exp_pcf.push(SeHdrFlag::ConfidentialDump);
let mut pcf_overrides = FlagsOverride::new();
for flag in &exp_pcf {
pcf_overrides.enable(*flag);
}
let expected_pcf = SeHdrControlFlagsModel::pcf_for_target(SeTarget::V1Max)
.with_overrides(&pcf_overrides)
.expect("Failed to create expected PCF");
assert_eq!(parsed_flags.0, expected_pcf);
// Build expected SCF
let exp_scf = vec![SeHdrFlag::CckUpdate];
let mut scf_overrides = FlagsOverride::new();
for flag in &exp_scf {
scf_overrides.enable(*flag);
}
let expected_scf = SeHdrControlFlagsModel::scf_for_target(SeTarget::V1Max)
.with_overrides(&scf_overrides)
.expect("Failed to create expected SCF");
assert_eq!(parsed_flags.1, expected_scf);
}
#[test]
fn test_auto_detect_version_v1() {
// Mock non-hybrid keys - should return V1
// Note: This is a simplified test. In real usage, you'd need actual HostKey instances
let keys: Vec<HostKey> = vec![];
let version = auto_detect_version(&keys);
assert_eq!(version, SeHdrVersion::V1);
}
#[test]
fn test_determine_version_explicit_v1() {
let keys: Vec<HostKey> = vec![];
let version = determine_version(HdrVersionSelection::Explicit(HdrVersion::V1), &keys);
assert_eq!(version, SeHdrVersion::V1);
}
#[test]
fn test_determine_version_explicit_v2() {
let keys: Vec<HostKey> = vec![];
let version = determine_version(HdrVersionSelection::Explicit(HdrVersion::V2), &keys);
assert_eq!(version, SeHdrVersion::V2);
}
#[test]
fn test_determine_version_auto_detect() {
let keys: Vec<HostKey> = vec![];
let version = determine_version(HdrVersionSelection::Auto, &keys);
// With empty keys, should default to V1
assert_eq!(version, SeHdrVersion::V1);
}
#[test]
fn parse_flags_with_disable_flags_no_conflict() {
let args = CreateBootImageArgs {
flags: vec![SeHdrFlagName::ConfidentialDump],
disable_flags: vec![SeHdrFlagName::PckmoHmac],
..Default::default()
};
let result = super::parse_flags(&args, SeHdrVersion::V1);
assert!(result.is_ok());
let (pcf, scf) = result.unwrap();
// ConfidentialDump should be enabled
assert!(pcf.has(SeHdrFlag::ConfidentialDump));
// PckmoHmac should be disabled
assert!(!pcf.has(SeHdrFlag::PckmoHmac));
assert_eq!(pcf.to_u64(), 0b100000000000000000000011100000_u64);
assert_eq!(scf.to_u64(), 0b0_u64);
}
#[test]
fn parse_flags_with_multiple_disable_flags() {
let args = CreateBootImageArgs {
disable_flags: vec![SeHdrFlagName::PckmoHmac, SeHdrFlagName::BackupTargetKeys],
..Default::default()
};
let result = super::parse_flags(&args, SeHdrVersion::V1);
assert!(result.is_ok());
let (pcf, _scf) = result.unwrap();
// Both flags should be disabled
assert!(!pcf.has(SeHdrFlag::PckmoHmac));
assert!(!pcf.has(SeHdrFlag::BackupTargetKeys));
}
#[test]
fn parse_flags_with_only_disable_flags() {
let args = CreateBootImageArgs {
disable_flags: vec![SeHdrFlagName::PckmoDeaTdea, SeHdrFlagName::PckmoAes],
..Default::default()
};
let result = super::parse_flags(&args, SeHdrVersion::V1);
assert!(result.is_ok());
let (pcf, _scf) = result.unwrap();
// PCKMO DEA/TDEA and AES should be disabled
assert!(!pcf.has(SeHdrFlag::PckmoDeaTdea));
assert!(!pcf.has(SeHdrFlag::PckmoAes));
}
#[test]
fn parse_flags_enable_and_disable_different_flags() {
let args = CreateBootImageArgs {
flags: vec![
SeHdrFlagName::ConfidentialDump,
SeHdrFlagName::BackupTargetKeys,
],
disable_flags: vec![
SeHdrFlagName::PckmoHmac,
SeHdrFlagName::NoComponentEncryption,
],
..Default::default()
};
let result = super::parse_flags(&args, SeHdrVersion::V1);
assert!(result.is_ok());
let (pcf, _scf) = result.unwrap();
// Enabled flags should be set
assert!(pcf.has(SeHdrFlag::ConfidentialDump));
assert!(pcf.has(SeHdrFlag::BackupTargetKeys));
// Disabled flags should not be set
assert!(!pcf.has(SeHdrFlag::PckmoHmac));
assert!(!pcf.has(SeHdrFlag::NoComponentEncryption));
}
}

View File

@@ -6,10 +6,12 @@ use std::path::Path;
use anyhow::Result;
use log::{info, warn};
use pv::misc::{open_file, read_certs, read_file};
use pv::misc::open_file;
use pv::request::NoVerifyHkd;
use pv::{FileAccessErrorType, PvCoreError};
use pvimg::error::{Error, OwnExitCode, PvError};
use pvimg::error::{Error, OwnExitCode};
use pvimg::uvdata::{KeyExchangeTrait, SeHdr, UvKeyHashesV1};
use utils::hkd::{HkdLoader, HkdVersionSelection};
use utils::HexSlice;
use crate::cli::TestArgs;
@@ -31,32 +33,25 @@ fn hdr_test_target_hashes(hdr: &SeHdr, key_hashes: &Path) -> Result<bool> {
err => Error::PvCore(err),
})?;
let hashes = UvKeyHashesV1::read_from_io(file)?;
let mut contains = hdr.contains_hash(&hashes.pchkh);
if contains {
log_println!(
" ✓ Host key hash {:#} is included",
HexSlice::from(&hashes.pchkh)
);
}
if hdr.contains_hash(&hashes.pbhkh) {
log_println!(
" ✓ Backup host key hash {:#} is included",
HexSlice::from(&hashes.pbhkh)
);
contains = true;
};
for hash in hashes.res {
if hdr.contains_hash(&hash) {
log_println!(" ✓ Key hash {:#} is included", HexSlice::from(&hash));
contains = true;
}
}
if !contains {
let matches = hashes.matching_hashes(hdr);
if matches.is_empty() {
warn!(" ✘ None of the key hashes is included");
Ok(false)
} else {
for m in matches {
match m.idx.kind() {
Some(kind) => {
log_println!(" ✓ {kind} {:#} is included", HexSlice::from(&m.hash))
}
None => log_println!(
" ✓ Key hash {:#} is included (zero-based index {})",
HexSlice::from(&m.hash),
m.idx.index()
),
}
}
Ok(true)
}
Ok(contains)
}
/// Returns `Ok(true)` if at least one of the given public key of the host key
@@ -72,27 +67,21 @@ where
let mut result = false;
for path in host_key_documents {
let hkd_path = path.as_ref();
let hkd_data = read_file(hkd_path, "host key document")?;
let certs = read_certs(&hkd_data)?;
if certs.is_empty() {
return Err(PvError::NoHkdInFile(hkd_path.display().to_string()).into());
}
if certs.len() != 1 {
warn!("The host key document in '{}' contains more than one certificate! Only the first certificate will be used.",
hkd_path.display());
}
// Panic: len is == 1 -> unwrap will succeed/not panic
let cert = certs.first().unwrap();
if hdr.contains(cert.public_key()?)? {
let hkd = HkdLoader::load_and_verify(
path,
&NoVerifyHkd,
HkdVersionSelection::Explicit(hdr.common.version.into()),
)?;
if hdr.contains(hkd)? {
result = true;
log_println!(" ✓ Host key document '{}' is included", hkd_path.display());
log_println!(
" ✓ Host key document '{}' is included",
path.as_ref().display()
);
} else {
log_println!(
" ✘ Host key document '{}' is not included",
hkd_path.display()
path.as_ref().display()
);
}
}

View File

@@ -36,11 +36,12 @@ pub mod misc {
pub mod uvdata {
pub use crate::pv_utils::{
AeadPlainDataTrait, BuilderTrait, ComponentMetadataV1, ControlFlagTrait, ControlFlagsTrait,
EnvelopeSeHdrV1, FlagData, KeyExchangeTrait, PcfV1, PlaintextControlFlagsV1, ScfV1, SeH,
SeHdr, SeHdrAadV1, SeHdrBinV1, SeHdrBuilder, SeHdrData, SeHdrDataV1, SeHdrPlain,
SeHdrVersion, SeHdrVersioned, SecretControlFlagsV1, UvDataPlainTrait, UvDataTrait,
UvKeyHashesV1,
AeadPlainDataTrait, BuilderTrait, ComponentMetadataV1, ControlFlagTrait,
EffectiveControlFlags, EnvelopeSeHdrV1, FlagData, FlagState, FlagsOverride,
IntoEnumIterator, KeyExchangeTrait, SeH, SeHdr, SeHdrAadV1, SeHdrBinV1, SeHdrBuilder,
SeHdrControlFlags, SeHdrControlFlagsModel, SeHdrData, SeHdrDataV1, SeHdrDataV2, SeHdrFlag,
SeHdrPlain, SeHdrVersion, SeHdrVersioned, SeTarget, UnknownFlags, UvDataPlainTrait,
UvDataTrait, UvKeyHashesV1,
};
}

View File

@@ -18,9 +18,10 @@ pub use layout::{Interval, Layout};
pub use misc::{round_up, try_copy_slice_to_array};
pub use psw::{ShortPsw, PSW, PSW_MASK_BA, PSW_MASK_EA};
pub use se_hdr::{
ComponentMetadataV1, ControlFlagTrait, ControlFlagsTrait, EnvelopeSeHdrV1, FlagData, PcfV1,
PlaintextControlFlagsV1, ScfV1, SeH, SeHdr, SeHdrAadV1, SeHdrBinV1, SeHdrBuilder, SeHdrData,
SeHdrDataV1, SeHdrPlain, SeHdrVersion, SeHdrVersioned, SecretControlFlagsV1,
ComponentMetadataV1, ControlFlagTrait, EffectiveControlFlags, EnvelopeSeHdrV1, FlagData,
FlagState, FlagsOverride, IntoEnumIterator, SeH, SeHdr, SeHdrAadV1, SeHdrBinV1, SeHdrBuilder,
SeHdrControlFlags, SeHdrControlFlagsModel, SeHdrData, SeHdrDataV1, SeHdrDataV2, SeHdrFlag,
SeHdrPlain, SeHdrVersion, SeHdrVersioned, SeTarget, UnknownFlags,
};
pub use secured_comp::{ComponentTrait, SecuredComponent, SecuredComponentBuilder};
pub use serializing::{bytesize, serialize_to_bytes};

View File

@@ -126,6 +126,12 @@ pub enum Error {
max_output_size: usize,
},
#[error("Operation {operation} not supported")]
UnsupportedOperation { operation: String },
#[error("Unsupported SE header version: {0:?}")]
UnsupportedSeHdrVersion(pv::request::SeHdrVersion),
// Errors from other crates
#[error(transparent)]
Deku(#[from] deku::DekuError),

View File

@@ -5,16 +5,19 @@
mod brb;
mod builder;
mod flags;
mod generic_flags;
mod hdr_v1;
mod hdr_v2;
mod keys;
pub use brb::{
ComponentMetadata, ComponentMetadataV1, EnvelopeSeHdrV1, SeH, SeHdr, SeHdrBinV1, SeHdrData,
SeHdrDataV1, SeHdrPlain, SeHdrVersion, SeHdrVersioned,
ComponentMetadata, ComponentMetadataV1, EnvelopeSeHdrV1, SeH, SeHdr, SeHdrBinV1, SeHdrBinV2,
SeHdrData, SeHdrDataV1, SeHdrDataV2, SeHdrPlain, SeHdrVersion, SeHdrVersioned,
};
pub use builder::SeHdrBuilder;
pub use flags::{
ControlFlagTrait, ControlFlagsTrait, FlagData, PcfV1, PlaintextControlFlagsV1, ScfV1,
SecretControlFlagsV1,
ControlFlagTrait, EffectiveControlFlags, FlagData, FlagState, FlagsOverride, SeHdrControlFlags,
SeHdrControlFlagsModel, SeHdrFlag, SeTarget,
};
pub use generic_flags::{IntoEnumIterator, UnknownFlags};
pub use hdr_v1::SeHdrAadV1;

View File

@@ -8,14 +8,15 @@ use std::mem::size_of;
use deku::ctx::Endian;
use deku::prelude::*;
use enum_dispatch::enum_dispatch;
use pv::request::openssl::pkey::{PKey, PKeyRef, Private, Public};
use pv::request::openssl::pkey::{PKey, Private, Public};
use pv::request::{seek_se_hdr_start, Aes256XtsKey, Confidential, SymKey, SymKeyType};
use pv::static_assert;
use serde::{Deserialize, Serialize};
use utils::S390ToolsMetaData;
use utils::{HkdVersion, S390ToolsMetaData};
pub use super::hdr_v1::{SeHdrBinV1, SeHdrDataV1};
use super::{PlaintextControlFlagsV1, SecretControlFlagsV1};
pub use super::hdr_v2::{SeHdrBinV2, SeHdrDataV2};
use super::{EffectiveControlFlags, SeHdrFlag};
use crate::misc::PAGESIZE;
use crate::pv_utils::error::{Error, Result};
use crate::pv_utils::misc::display_indented;
@@ -60,6 +61,17 @@ impl EnvelopeSeHdrV1 {
pub enum SeHdrVersion {
/// Secure Execution header v1
V1 = 0x100,
/// Secure Execution header v2
V2 = 0x200,
}
impl From<SeHdrVersion> for HkdVersion {
fn from(val: SeHdrVersion) -> Self {
match val {
SeHdrVersion::V1 => Self::Classical,
SeHdrVersion::V2 => Self::Hybrid,
}
}
}
impl Display for SeHdrVersion {
@@ -69,6 +81,7 @@ impl Display for SeHdrVersion {
"{}",
match self {
SeHdrVersion::V1 => "1",
SeHdrVersion::V2 => "2",
}
)
}
@@ -204,13 +217,53 @@ impl Display for SeHdrPlain {
}
}
#[enum_dispatch(AeadCipherTrait, AeadDataTrait, KeyExchangeTrait)]
#[non_exhaustive]
#[enum_dispatch(AeadCipherTrait, AeadDataTrait)]
#[derive(Clone, PartialEq, Eq, Debug, DekuRead, DekuWrite, Serialize, Deserialize)]
#[serde(untagged)]
#[deku(ctx = "_endian: Endian, version: SeHdrVersion", id = "version")]
pub enum SeHdrVersioned {
#[deku(id = "SeHdrVersion::V1")]
SeHdrBinV1(SeHdrBinV1),
#[deku(id = "SeHdrVersion::V2")]
SeHdrBinV2(SeHdrBinV2),
}
impl KeyExchangeTrait for SeHdrVersioned {
type PrivateKeyType = PKey<Private>;
type TargetKeyType = pv::request::HostKey;
fn contains<K>(&self, key: K) -> Result<bool>
where
K: AsRef<Self::TargetKeyType>,
{
match (self, key.as_ref()) {
(SeHdrVersioned::SeHdrBinV1(data), pv::request::HostKey::V1(key)) => data.contains(key),
(SeHdrVersioned::SeHdrBinV2(data), pv::request::HostKey::V2(key)) => data.contains(key),
(_, _) => Err(Error::InvalidSeHdr),
}
}
fn contains_hash<H: AsRef<[u8]>>(&self, hash: H) -> bool {
match self {
SeHdrVersioned::SeHdrBinV1(data) => data.contains_hash(hash),
SeHdrVersioned::SeHdrBinV2(data) => data.contains_hash(hash),
}
}
fn cust_pub_key(&mut self) -> Result<PKey<Public>> {
match self {
SeHdrVersioned::SeHdrBinV1(data) => data.cust_pub_key(),
SeHdrVersioned::SeHdrBinV2(data) => data.cust_pub_key(),
}
}
fn key_type(&self) -> SymKeyType {
match self {
SeHdrVersioned::SeHdrBinV1(data) => data.key_type(),
SeHdrVersioned::SeHdrBinV2(data) => data.key_type(),
}
}
}
impl Display for SeHdrVersioned {
@@ -223,10 +276,18 @@ impl Display for SeHdrVersioned {
write!(f, "{se_hdr_bin_v1}")
}
}
SeHdrVersioned::SeHdrBinV2(se_hdr_bin_v2) => {
if f.alternate() {
write!(f, "{se_hdr_bin_v2:#}")
} else {
write!(f, "{se_hdr_bin_v2}")
}
}
}
}
}
#[non_exhaustive]
#[enum_dispatch(
AeadCipherTrait,
AeadPlainDataTrait,
@@ -239,6 +300,8 @@ impl Display for SeHdrVersioned {
pub enum SeHdrData {
#[deku(id = "SeHdrVersion::V1")]
SeHdrDataV1(SeHdrDataV1),
#[deku(id = "SeHdrVersion::V2")]
SeHdrDataV2(SeHdrDataV2),
}
impl Display for SeHdrData {
@@ -251,6 +314,13 @@ impl Display for SeHdrData {
write!(f, "{data_v1}")
}
}
SeHdrData::SeHdrDataV2(data_v2) => {
if f.alternate() {
write!(f, "{data_v2:#}")
} else {
write!(f, "{data_v2}")
}
}
}
}
}
@@ -259,6 +329,7 @@ impl AeadCipherBuilderTrait for SeHdrData {
fn set_iv(&mut self, iv: &[u8]) -> Result<()> {
match self {
Self::SeHdrDataV1(data) => data.set_iv(iv),
Self::SeHdrDataV2(data) => data.set_iv(iv),
}
}
}
@@ -267,7 +338,7 @@ impl AeadCipherBuilderTrait for SeHdrData {
pub trait SeHdrPubBuilderTrait {
// Payload related methods
fn set_components(&mut self, meta: ComponentMetadata) -> Result<()>;
fn set_pcf(&mut self, pcf: &PlaintextControlFlagsV1) -> Result<()>;
fn set_pcf(&mut self, pcf: &EffectiveControlFlags<SeHdrFlag>) -> Result<()>;
}
#[allow(dead_code)]
@@ -276,7 +347,7 @@ pub trait SeHdrConfBuilderTrait {
fn generate_cck(&self) -> Result<SymKey>;
fn set_cck(&mut self, cck: Confidential<Vec<u8>>) -> Result<()>;
fn set_psw(&mut self, psw: &PSW);
fn set_scf(&mut self, scf: &SecretControlFlagsV1) -> Result<()>;
fn set_scf(&mut self, scf: &EffectiveControlFlags<SeHdrFlag>) -> Result<()>;
}
#[enum_dispatch(SeHdr)]
@@ -316,14 +387,13 @@ impl AeadDataTrait for SeHdr {
}
impl KeyExchangeTrait for SeHdr {
type PrivateKeyType = PKey<Private>;
type TargetKeyType = pv::request::HostKey;
fn contains_hash<H: AsRef<[u8]>>(&self, hash: H) -> bool {
self.data.contains_hash(hash)
}
fn contains<K: AsRef<PKeyRef<Public>>>(&self, key: K) -> Result<bool> {
self.data.contains(key)
}
fn cust_pub_key(&mut self) -> Result<PKey<Public>> {
self.data.cust_pub_key()
}
@@ -331,6 +401,13 @@ impl KeyExchangeTrait for SeHdr {
fn key_type(&self) -> SymKeyType {
self.aead_key_type()
}
fn contains<K>(&self, key: K) -> Result<bool>
where
K: AsRef<Self::TargetKeyType>,
{
self.data.contains(key)
}
}
impl UvDataTrait for SeHdr {
@@ -407,25 +484,46 @@ impl AeadCipherBuilderTrait for SeHdrPlain {
}
impl KeyExchangeBuilderTrait for SeHdrPlain {
type AeadKeyType = SymKey;
type PrivateKeyType = PKey<Private>;
type TargetKeyType = pv::request::HostKey;
fn add_keyslot(
&mut self,
hostkey: &PKeyRef<Public>,
aead_key: &SymKey,
priv_key: &PKeyRef<Private>,
hostkey: &Self::TargetKeyType,
aead_key: &Self::AeadKeyType,
priv_key: &Self::PrivateKeyType,
) -> Result<()> {
self.data.add_keyslot(hostkey, aead_key, priv_key)
match (&mut self.data, hostkey) {
(SeHdrData::SeHdrDataV1(data), pv::request::HostKey::V1(key)) => {
data.add_keyslot(key, aead_key, priv_key)
}
(SeHdrData::SeHdrDataV2(data), pv::request::HostKey::V2(key)) => {
data.add_keyslot(key, aead_key, priv_key)
}
(_, _) => Err(Error::InvalidSeHdr),
}
}
fn clear_keyslots(&mut self) -> Result<()> {
self.data.clear_keyslots()
match &mut self.data {
SeHdrData::SeHdrDataV1(data) => data.clear_keyslots(),
SeHdrData::SeHdrDataV2(data) => data.clear_keyslots(),
}
}
fn generate_private_key(&self) -> Result<PKey<Private>> {
self.data.generate_private_key()
match &self.data {
SeHdrData::SeHdrDataV1(data) => data.generate_private_key(),
SeHdrData::SeHdrDataV2(data) => data.generate_private_key(),
}
}
fn set_cust_public_key(&mut self, key: &PKeyRef<Private>) -> Result<()> {
self.data.set_cust_public_key(key)
fn set_cust_public_key(&mut self, key: &Self::PrivateKeyType) -> Result<()> {
match &mut self.data {
SeHdrData::SeHdrDataV1(data) => data.set_cust_public_key(key),
SeHdrData::SeHdrDataV2(data) => data.set_cust_public_key(key),
}
}
}
@@ -451,20 +549,39 @@ pub enum ComponentMetadata {
}
impl KeyExchangeTrait for SeHdrPlain {
fn contains<K: AsRef<PKeyRef<Public>>>(&self, key: K) -> Result<bool> {
self.data.contains(key)
}
type PrivateKeyType = PKey<Private>;
type TargetKeyType = pv::request::HostKey;
fn cust_pub_key(&mut self) -> Result<PKey<Public>> {
self.data.cust_pub_key()
match &mut self.data {
SeHdrData::SeHdrDataV1(data) => data.cust_pub_key(),
SeHdrData::SeHdrDataV2(data) => data.cust_pub_key(),
}
}
fn key_type(&self) -> SymKeyType {
self.data.key_type()
match &self.data {
SeHdrData::SeHdrDataV1(data) => data.key_type(),
SeHdrData::SeHdrDataV2(data) => data.key_type(),
}
}
fn contains_hash<H: AsRef<[u8]>>(&self, hash: H) -> bool {
self.data.contains_hash(hash)
match &self.data {
SeHdrData::SeHdrDataV1(data) => data.contains_hash(hash),
SeHdrData::SeHdrDataV2(data) => data.contains_hash(hash),
}
}
fn contains<K>(&self, key: K) -> Result<bool>
where
K: AsRef<Self::TargetKeyType>,
{
match (&self.data, key.as_ref()) {
(SeHdrData::SeHdrDataV1(data), pv::request::HostKey::V1(key)) => data.aad.contains(key),
(SeHdrData::SeHdrDataV2(data), pv::request::HostKey::V2(key)) => data.aad.contains(key),
(_, _) => Err(Error::InvalidSeHdr),
}
}
}

View File

@@ -5,7 +5,8 @@
use pv::request::Confidential;
use super::hdr_v1::SeHdrDataV1;
use super::SeHdr;
use super::hdr_v2::SeHdrDataV2;
use super::{EffectiveControlFlags, SeHdr, SeHdrFlag};
use crate::pv_utils::error::{Error, Result};
use crate::pv_utils::se_hdr::brb::{
SeHdrCommon, SeHdrConfBuilderTrait, SeHdrData, SeHdrPubBuilderTrait,
@@ -15,10 +16,10 @@ use crate::pv_utils::uvdata::UvDataPlainTrait;
use crate::pv_utils::uvdata_builder::{
AeadCipherBuilderTrait, BuilderTrait, KeyExchangeBuilderTrait, UvDataBuilder,
};
use crate::pv_utils::{PlaintextControlFlagsV1, SecretControlFlagsV1, PSW};
use crate::pv_utils::PSW;
/// `SeHdrBuilder`
pub type SeHdrBuilder<'a> = UvDataBuilder<'a, SeHdrPlain>;
pub type SeHdrBuilder<'a> = UvDataBuilder<SeHdrPlain>;
impl SeHdrBuilder<'_> {
pub fn new<M: Into<ComponentMetadata>>(
@@ -40,6 +41,19 @@ impl SeHdrBuilder<'_> {
data.set_cust_public_key(&priv_key)?;
(SeHdrData::SeHdrDataV1(data), aead_key, priv_key)
}
SeHdrVersion::V2 => {
let mut data = SeHdrDataV2::new(
psw,
components_meta
.into()
.try_into()
.map_err(|_| Error::InvalidComponentMetadata)?,
)?;
let aead_key = data.generate_aead_key()?;
let priv_key = data.generate_private_key()?;
data.set_cust_public_key(&priv_key)?;
(SeHdrData::SeHdrDataV2(data), aead_key, priv_key)
}
};
let common = SeHdrCommon::new(version);
let hdr = SeHdrPlain { common, data };
@@ -67,12 +81,12 @@ impl SeHdrBuilder<'_> {
Ok(self)
}
pub fn with_pcf(&mut self, flags: &PlaintextControlFlagsV1) -> Result<&mut Self> {
pub fn with_pcf(&mut self, flags: &EffectiveControlFlags<SeHdrFlag>) -> Result<&mut Self> {
self.plain_data.data.set_pcf(flags)?;
Ok(self)
}
pub fn with_scf(&mut self, flags: &SecretControlFlagsV1) -> Result<&mut Self> {
pub fn with_scf(&mut self, flags: &EffectiveControlFlags<SeHdrFlag>) -> Result<&mut Self> {
self.plain_data.data.set_scf(flags)?;
Ok(self)
}
@@ -95,8 +109,8 @@ impl BuilderTrait for SeHdrBuilder<'_> {
mod tests {
use std::io::Cursor;
use pv::request::{Confidential, SymKeyType, SHA_512_HASH_LEN};
use pv::test_utils::get_test_key_and_cert;
use pv::request::{Confidential, HostKey, HybridPKey, SymKeyType, SHA_512_HASH_LEN};
use pv::test_utils::{get_test_key_and_cert, get_test_key_and_cert_hybrid};
use super::*;
use crate::pv_utils::se_hdr::ComponentMetadataV1;
@@ -108,7 +122,7 @@ mod tests {
use pv::test_utils::get_test_key_and_cert;
let (cust_key, host_key) = get_test_key_and_cert();
let host_keys = [host_key.public_key().unwrap()];
let host_keys = [HostKey::V1(host_key.public_key().unwrap())];
let xts_key = Confidential::new([0x3; SymKeyType::AES_256_XTS_KEY_LEN]);
let xts_key2 = Confidential::new([0x3; SymKeyType::AES_256_XTS_KEY_LEN]);
let mut builder = SeHdrBuilder::new(
@@ -236,10 +250,140 @@ mod tests {
let _decrypted_hdrv1: SeHdrDataV1 = decrypted.data.try_into().expect("BUG");
}
#[test]
fn builder_test_v2() {
use pv::test_utils::get_test_key_and_cert_hybrid;
let (cust_key, host_key1, host_key2) = get_test_key_and_cert_hybrid();
let host_keys = [HostKey::V2(
HybridPKey::new(
host_key1.public_key().unwrap(),
host_key2.public_key().unwrap(),
)
.unwrap(),
)];
let xts_key = Confidential::new([0x3; SymKeyType::AES_256_XTS_KEY_LEN]);
let xts_key2 = Confidential::new([0x3; SymKeyType::AES_256_XTS_KEY_LEN]);
let mut builder = SeHdrBuilder::new(
SeHdrVersion::V2,
PSW {
addr: 1234,
mask: 5678,
},
ComponentMetadata::ComponentMetadataV1(ComponentMetadataV1 {
ald: [0x1; SHA_512_HASH_LEN],
pld: [0x2; SHA_512_HASH_LEN],
tld: [0x3; SHA_512_HASH_LEN],
nep: 1,
key: xts_key,
}),
)
.expect("should not fail");
// builder.add_comp_data(addr, tweak, )?;
builder
.with_components(ComponentMetadataV1 {
ald: [0x1; SHA_512_HASH_LEN],
pld: [0x2; SHA_512_HASH_LEN],
tld: [0x3; SHA_512_HASH_LEN],
nep: 1,
key: xts_key2,
})
.expect("should not fail");
builder
.with_priv_key(&cust_key)
.expect_err("Error expected as expert mode is not enabled");
builder.expert_mode = true;
builder.with_priv_key(&cust_key).expect("should not fail");
// Set CCK
// Too large key
builder
.with_cck([49; SymKeyType::AES_256_GCM_KEY_LEN - 1].to_vec().into())
.expect_err("should fail");
// Too small key
builder
.with_cck([49; SymKeyType::AES_256_GCM_KEY_LEN + 1].to_vec().into())
.expect_err("should fail");
builder
.with_cck([49; SymKeyType::AES_256_GCM_KEY_LEN].to_vec().into())
.expect("should not fail");
// Set protection key
// Too large key
builder
.with_aead_key(Confidential::new([50; 33].into()))
.expect_err("should fail");
// Too small key
builder
.with_aead_key(Confidential::new([50; 31].into()))
.expect_err("should fail");
builder
.with_aead_key(Confidential::new([50; 32].into()))
.expect("should not fail");
// Set IV
// Too large IV
builder.with_iv(&[51; 13]).expect_err("should fail");
// Too small IV
builder.with_iv(&[51; 11]).expect_err("should fail");
builder.with_iv(&[51; 12]).expect("should not fail");
builder.add_hostkeys(&host_keys).expect("should not fail");
let prot_key = builder.prot_key().clone();
let bin = builder.build().expect("wuhu");
assert_eq!(bin.common.version, SeHdrVersion::V2);
assert_eq!(bin.as_bytes().expect("should not fail").len(), 2240);
assert_eq!(
bin.as_bytes().expect("should not fail")[..480],
[
73, 66, 77, 83, 101, 99, 69, 120, 0, 0, 2, 0, 0, 0, 8, 192, 51, 51, 51, 51, 51, 51,
51, 51, 51, 51, 51, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
128, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 199, 93, 52, 249, 22, 82, 219, 69, 123, 11, 32, 156, 70, 164, 145,
164, 78, 226, 177, 110, 35, 194, 216, 218, 241, 22, 103, 138, 98, 242, 76, 227, 50,
197, 153, 95, 8, 69, 107, 102, 177, 109, 213, 90, 146, 197, 7, 241, 227, 26, 247,
140, 100, 168, 46, 122, 84, 27, 21, 19, 80, 21, 242, 2, 134, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 64, 128, 88, 167, 241, 165, 195, 80, 151, 83, 58, 2, 169,
56, 121, 231, 222, 103, 186, 40, 11, 206, 131, 101, 236, 148, 178, 185, 8, 245,
137, 195, 169, 152, 216, 190, 30, 99, 7, 215, 74, 224, 26, 220, 70, 130, 95, 246,
187, 111, 160, 92, 17, 71, 207, 226, 204, 244, 162, 79, 61, 131, 61, 218, 112, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 255, 94, 191,
53, 220, 196, 47, 37, 93, 227, 234, 101, 1, 174, 171, 68, 42, 136, 92, 238, 72, 6,
17, 77, 231, 225, 174, 22, 222, 188, 212, 15, 248, 145, 72, 126, 139, 17, 233, 225,
156, 46, 233, 151, 54, 2, 175, 88, 215, 254, 243, 222, 37, 81, 50, 110, 18, 76,
252, 12, 210, 146, 66, 23
][..480]
);
let decrypted = bin.decrypt(&prot_key).expect("BUG");
assert_eq!(bin.common, decrypted.common);
assert_eq!(
bin.aad().expect("should not fail"),
decrypted.aad().expect("should not fail")
);
assert_ne!(
&bin.data(),
decrypted.data().expect("should not fail").value()
);
let _decrypted_hdrv2: SeHdrDataV2 = decrypted.data.try_into().expect("BUG");
}
#[test]
fn chain_test() {
let (_, host_key) = get_test_key_and_cert();
let host_keys = [host_key.public_key().unwrap()];
let host_keys = [HostKey::V1(host_key.public_key().unwrap())];
let xts_key = Confidential::new([0x3; SymKeyType::AES_256_XTS_KEY_LEN]);
let meta = ComponentMetadataV1 {
ald: [0x1; SHA_512_HASH_LEN],
@@ -274,4 +418,49 @@ mod tests {
assert_eq!(hdr_plain.common.version, hdr.common.version);
let _hdr_data_v1: SeHdrDataV1 = hdr_plain.data.try_into().expect("should not fail");
}
#[test]
fn chain_test_v2() {
let (_, host_key1, host_key2) = get_test_key_and_cert_hybrid();
let host_keys = [HostKey::V2(
HybridPKey::new(
host_key1.public_key().unwrap(),
host_key2.public_key().unwrap(),
)
.unwrap(),
)];
let xts_key = Confidential::new([0x3; SymKeyType::AES_256_XTS_KEY_LEN]);
let meta = ComponentMetadataV1 {
ald: [0x1; SHA_512_HASH_LEN],
pld: [0x2; SHA_512_HASH_LEN],
tld: [0x3; SHA_512_HASH_LEN],
nep: 3,
key: xts_key,
};
let cck = Confidential::new([0x42; 32].to_vec());
let mut builder = SeHdrBuilder::new(
SeHdrVersion::V2,
PSW {
addr: 1234,
mask: 5678,
},
meta,
)
.expect("should not fail");
let prot_key = builder.prot_key().to_owned();
builder
.add_hostkeys(&host_keys)
.expect("should not fail")
.with_cck(cck)
.expect("should not fail");
let bin = builder.build().expect("should not fail");
let reader = Cursor::new(bin.as_bytes().expect("should not fail"));
let hdr = SeHdr::try_from_io(reader).unwrap();
let hdr_plain = hdr.decrypt(&prot_key).unwrap();
assert_eq!(hdr_plain.common.version, SeHdrVersion::V2);
assert_eq!(hdr_plain.common.version, hdr.common.version);
let _hdr_data_v2: SeHdrDataV2 = hdr_plain.data.try_into().expect("should not fail");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,878 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp.
//! Generic control flags infrastructure for Secure Execution (SE) headers.
//!
//! This module provides the generic, reusable components for managing control flags
//! in IBM Secure Execution headers. It defines traits, generic types, and implementations
//! that can work with any flag enum type.
//!
//! # Core Components
//!
//! ## Traits
//!
//! - [`ControlFlagTrait`] - Core trait that flag enums must implement
//! - [`IntoEnumIterator`] - Enables iteration over flag enum variants
//! - [`IntoBitPosition`] - Converts types to bit positions
//!
//! ## Generic Types
//!
//! - [`ControlFlagsModel<T>`] - Version-specific configuration model
//! - [`EffectiveControlFlags<T>`] - Final flags after applying overrides
//! - [`FlagsOverride<T>`] - Container for user-specified overrides
//! - [`FlagData<T>`] - Pairs a flag with its state
//! - [`UnknownFlags`] - Tracks unknown/unsupported flag bits
//!
//! ## Supporting Types
//!
//! - [`FlagState`] - Enabled or Disabled state
//! - [`FlagValidationError<T>`] - Validation errors
//! - [`Msb0FlagsConversionError<T>`] - Conversion errors
//!
//! # Usage
//!
//! This module is not typically used directly. Instead, use the concrete implementations
//! in the [`flags`](super::flags) module which provide `SeHdrFlag`, `SeHdrControlFlagsModel`,
//! and related types built on top of this generic infrastructure.
use std::collections::{HashMap, HashSet};
use std::fmt::{Debug, Display};
use std::hash::Hash;
use pv::misc::{Flags, Msb0Flags64};
use super::brb::SeHdrVersion;
#[derive(Debug)]
pub enum FlagValidationError<T: ControlFlagTrait> {
UnknownFlag(),
NotSupported { model: &'static str, flag: T },
}
impl<T: ControlFlagTrait> Display for FlagValidationError<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FlagValidationError::UnknownFlag() => write!(f, "Unknown flag"),
FlagValidationError::NotSupported { model, flag } => {
write!(f, "Flag '{}' is not supported in {}", flag, model)
}
}
}
}
impl<T: ControlFlagTrait> std::error::Error for FlagValidationError<T> {}
/// Secure Execution target configuration.
///
/// Specifies the target environment for which a Secure Execution boot image is being created.
/// This enum allows selecting the appropriate control flags and configuration based on the
/// target machine generation or SE header version.
///
/// # Current Usage
///
/// Currently, this enum is primarily used for selecting control flags configurations.
/// The target determines which control flags are available and their default values.
///
/// # Purpose
///
/// The target determines:
/// - Which control flags are available and their default values
/// - The SE header format version to use
/// - Compatibility with specific machine generations (future use)
///
/// # Future Extensibility
///
/// This design allows for future expansion when multiple variants of V1 or V2 configurations
/// may exist.
///
/// # Examples
///
/// ```
/// use pvimg::uvdata::{SeHdrControlFlagsModel, SeTarget};
///
/// // Select latest V1 configuration for newest features
/// let target = SeTarget::V1Max;
///
/// // Use target to get appropriate control flags
/// let pcf = SeHdrControlFlagsModel::pcf_for_target(target);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum SeTarget {
/// Latest V1 configuration.
///
/// Targets the most recent V1 SE header format with all available V1 control flags.
/// Use this for maximum compatibility with older machine generations that support V1.
V1Max,
/// Latest V2 configuration.
///
/// Targets the most recent V2 SE header format with all available V2 control flags.
/// Use this for newest features and machine generations that support V2.
V2Max,
}
impl SeTarget {
/// Converts the target to the corresponding SeHdrVersion.
pub fn to_se_hdr_version(self) -> SeHdrVersion {
match self {
SeTarget::V1Max => SeHdrVersion::V1,
SeTarget::V2Max => SeHdrVersion::V2,
}
}
/// Creates a target from a SeHdrVersion.
pub fn from_se_hdr_version(version: SeHdrVersion) -> Self {
match version {
SeHdrVersion::V1 => SeTarget::V1Max,
SeHdrVersion::V2 => SeTarget::V2Max,
}
}
}
impl Display for SeTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SeTarget::V1Max => write!(f, "V1-max"),
SeTarget::V2Max => write!(f, "V2-max"),
}
}
}
pub trait IntoEnumIterator {
/// Returns an iterator over all variants of this enum.
fn iter() -> impl Iterator<Item = Self>;
}
/// Trait for individual control flag types.
///
/// This trait defines the interface for control flag enums, providing methods
/// to get the flag's bit position and create enabled/disabled flag data.
pub trait ControlFlagTrait:
Debug + Hash + Copy + Eq + Ord + Display + IntoEnumIterator + AsRef<Self>
{
/// Returns the bit position of this flag.
///
/// The bit position determines where this flag is set in the control flags bitfield.
fn bit_position(self) -> u8;
/// Creates flag data with this flag in the enabled state.
fn enabled(self) -> FlagData<Self> {
FlagData::new(self, FlagState::Enabled)
}
/// Creates flag data with this flag in the disabled state.
fn disabled(self) -> FlagData<Self> {
FlagData::new(self, FlagState::Disabled)
}
}
/// Internal state of a control flag (enabled or disabled).
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub enum FlagState {
/// Flag is enabled (bit set to 1)
Enabled,
/// Flag is disabled (bit set to 0)
Disabled,
}
/// Represents a control flag with its associated state.
///
/// This structure pairs a flag with its enabled/disabled state, used when
/// constructing or modifying `ControlFlags` instances.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct FlagData<T: ControlFlagTrait> {
value: T,
state: FlagState,
}
impl<T: ControlFlagTrait> FlagData<T> {
const fn new(value: T, state: FlagState) -> Self {
Self { value, state }
}
}
/// Generic flags configuration for a specific SE header flags target.
///
/// Contains the default and supported flags for a given target version.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ControlFlagsModel<T: ControlFlagTrait> {
target: SeTarget,
default: HashSet<T>,
supported: HashSet<T>,
}
impl<T: ControlFlagTrait> ControlFlagsModel<T> {
/// Creates a new Flags configuration.
pub(super) fn new(target: SeTarget, default: HashSet<T>, supported: HashSet<T>) -> Self {
Self {
target,
default,
supported,
}
}
/// Returns the SE header flags target.
pub fn target(&self) -> SeTarget {
self.target
}
/// Returns the SE header version corresponding to this target.
pub fn version(&self) -> SeHdrVersion {
self.target.to_se_hdr_version()
}
/// Returns the set of default flags for this version.
pub fn default_flags(&self) -> &HashSet<T> {
&self.default
}
/// Returns the set of supported flags for this version.
pub fn supported_flags(&self) -> &HashSet<T> {
&self.supported
}
/// Checks if a flag is supported in this version.
pub fn supports<F: AsRef<T>>(&self, flag: F) -> bool {
self.supported.contains(flag.as_ref())
}
/// Checks if a flag is a default flag in this version.
#[must_use]
pub fn is_default<F: AsRef<T>>(&self, flag: F) -> bool {
self.default.contains(flag.as_ref())
}
/// Validates that all flags in the overrides are supported by this model.
///
/// # Arguments
///
/// * `overrides` - The overrides to validate
///
/// # Errors
///
/// Returns `FlagValidationError::NotSupported` if any override flag is not supported by this
/// model
pub fn validate_overrides(
&self,
overrides: &FlagsOverride<T>,
) -> Result<(), FlagValidationError<T>> {
for (flag, _state) in overrides.iter() {
if !self.supports(flag) {
return Err(FlagValidationError::NotSupported {
model: std::any::type_name::<T>(),
flag: *flag,
});
}
}
Ok(())
}
/// Applies overrides to the default flags and returns the effective control flags.
///
/// This method validates the overrides, applies them to the default flags, and returns
/// an `EffectiveControlFlags` instance containing the resulting configuration.
///
/// # Arguments
///
/// * `overrides` - Overrides to apply to the default flags
///
/// # Returns
///
/// An `EffectiveControlFlags<T>` instance with the effective flags after applying overrides
///
/// # Errors
///
/// Returns `FlagValidationError::NotSupported` if an override flag is not supported by this
/// model
pub fn with_overrides(
&self,
overrides: &FlagsOverride<T>,
) -> Result<EffectiveControlFlags<T>, FlagValidationError<T>> {
// Validate overrides first
self.validate_overrides(overrides)?;
let mut effective_flags = self.default.clone();
// Apply overrides
for (flag, state) in overrides.iter() {
match state {
FlagState::Enabled => {
effective_flags.insert(*flag);
}
FlagState::Disabled => {
effective_flags.remove(flag);
}
}
}
Ok(EffectiveControlFlags {
version: self.version(),
known_flags: effective_flags,
unknown_flags: UnknownFlags::empty(),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnknownFlags(Msb0Flags64);
/// Trait for types that can be converted to a bit position.
pub trait IntoBitPosition {
fn into_bit_position(self) -> u8;
}
/// Implement IntoBitPosition for u8 directly
impl IntoBitPosition for u8 {
fn into_bit_position(self) -> u8 {
self
}
}
/// Implement IntoBitPosition for types that implement ControlFlagTrait
impl<T: ControlFlagTrait> IntoBitPosition for T {
fn into_bit_position(self) -> u8 {
self.bit_position()
}
}
impl UnknownFlags {
pub fn empty() -> Self {
Self(Msb0Flags64::from(0u64))
}
pub fn from_bits(bits: Msb0Flags64) -> Self {
Self(bits)
}
pub fn bits(self) -> u64 {
self.0.into()
}
#[must_use]
pub fn is_empty(self) -> bool {
self.bits() == 0
}
/// Checks if a specific flag or bit position is set in the unknown flags.
///
/// # Arguments
///
/// * `flag` - Either a flag that implements `ControlFlagTrait`, a reference to such a flag, or
/// a raw bit position (u8)
///
/// # Returns
///
/// `true` if the bit position is set in the unknown flags, `false` otherwise
///
/// # Examples
///
/// ```
/// use pv::misc::Msb0Flags64;
/// use pvimg::uvdata::UnknownFlags;
///
/// // Create UnknownFlags with bit 5 set
/// let flags = Msb0Flags64::from(1u64 << (63 - 5));
/// let unknown_flags = UnknownFlags::from_bits(flags);
///
/// // Using a raw bit position
/// assert!(unknown_flags.contains(5u8));
/// assert!(!unknown_flags.contains(10u8));
/// ```
#[must_use]
pub fn contains(&self, position: u8) -> bool {
self.0.is_set(position)
}
}
/// Represents the effective control flags after applying overrides.
///
/// This structure contains the final set of flags that will be used, including both
/// known flags (from the model) and any unknown flags that were present in the input.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EffectiveControlFlags<T: ControlFlagTrait> {
version: SeHdrVersion,
known_flags: HashSet<T>,
unknown_flags: UnknownFlags,
}
impl<T: ControlFlagTrait> EffectiveControlFlags<T> {
/// Creates a new EffectiveControlFlags instance.
///
/// This is primarily used internally by the flags module.
pub(super) fn new(
version: SeHdrVersion,
known_flags: HashSet<T>,
unknown_flags: UnknownFlags,
) -> Self {
Self {
version,
known_flags,
unknown_flags,
}
}
/// Returns the SE header version.
pub fn version(&self) -> SeHdrVersion {
self.version
}
/// Returns the set of known flags.
pub fn known_flags(&self) -> &HashSet<T> {
&self.known_flags
}
/// Returns the unknown flags.
pub fn unknown_flags(&self) -> UnknownFlags {
self.unknown_flags
}
/// Checks if a flag or bit position is enabled in the effective flags.
///
/// This method accepts either a flag type or a raw bit position (u8).
///
/// # Arguments
///
/// * `position` - Either a flag that implements `ControlFlagTrait`, a reference to such a flag,
/// or a raw bit position (u8)
///
/// # Returns
///
/// `true` if the flag/bit is enabled, `false` otherwise
///
/// # Examples
///
/// ```
/// use pvimg::uvdata::{SeHdrControlFlagsModel, SeHdrFlag, SeTarget};
///
/// // Get default flags for V1
/// let pcf_v1 = SeHdrControlFlagsModel::pcf_for_target(SeTarget::V1Max);
/// let flags = pcf_v1.with_overrides(&Default::default()).unwrap();
///
/// // Using a flag
/// assert!(flags.has(SeHdrFlag::PckmoAes));
///
/// // Using a raw bit position (PckmoAes is at bit 57)
/// assert!(flags.has(57u8));
/// ```
pub fn has<P: IntoBitPosition>(&self, position: P) -> bool {
let bit_pos = position.into_bit_position();
// Check if any known flag has this bit position
for flag in &self.known_flags {
if flag.bit_position() == bit_pos {
return true;
}
}
// Check unknown flags
self.unknown_flags.contains(bit_pos)
}
/// Converts the effective flags to a u64 value.
///
/// # Returns
///
/// A u64 representation combining both known and unknown flags
pub fn to_u64(&self) -> u64 {
let flags: Msb0Flags64 = self.into();
flags.into()
}
}
/// Converts `EffectiveControlFlags` to `Msb0Flags64`.
///
/// This combines both known and unknown flags into a single bitfield.
impl<T: ControlFlagTrait> From<EffectiveControlFlags<T>> for Msb0Flags64 {
fn from(flags: EffectiveControlFlags<T>) -> Self {
let mut value = Msb0Flags64::from(flags.unknown_flags.bits());
for flag in &flags.known_flags {
value.set_bit(flag.bit_position());
}
value
}
}
/// Converts a reference to `EffectiveControlFlags` to `Msb0Flags64`.
impl<T: ControlFlagTrait> From<&EffectiveControlFlags<T>> for Msb0Flags64 {
fn from(flags: &EffectiveControlFlags<T>) -> Self {
let mut value = Msb0Flags64::from(flags.unknown_flags.bits());
for flag in &flags.known_flags {
value.set_bit(flag.bit_position());
}
value
}
}
/// Display implementation for `EffectiveControlFlags<T>`.
///
/// Provides two display formats:
/// - Normal format: Lists all enabled flags with " - " prefix, one per line
/// - Alternate format (`{:#}`): Shows the raw bitfield as 66-character binary string
///
/// # Examples
///
/// ```rust
/// use pvimg::uvdata::{FlagsOverride, SeHdrControlFlagsModel, SeHdrFlag, SeTarget};
///
/// let pcf_v1 = SeHdrControlFlagsModel::pcf_for_target(SeTarget::V1Max);
/// let mut overrides = FlagsOverride::new();
/// overrides.enable(SeHdrFlag::ConfidentialDump);
/// let effective = pcf_v1.with_overrides(&overrides).unwrap();
///
/// // Normal format - lists enabled flags
/// println!("{}", effective);
///
/// // Alternate format - shows binary representation
/// println!("{:#}", effective);
/// ```
impl<T: ControlFlagTrait> Display for EffectiveControlFlags<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if f.alternate() {
// Alternate format: show as binary
let flags: Msb0Flags64 = self.into();
write!(f, "{:#066b}", <u64>::from(flags))
} else {
// Normal format: list enabled flags
let flags_s: Vec<String> = self
.known_flags
.iter()
.map(|flag| format!(" - {flag}"))
.collect();
if flags_s.is_empty() && self.unknown_flags.is_empty() {
write!(f, "(no flags enabled)")
} else {
let mut output = flags_s.join("\n");
if !self.unknown_flags.is_empty() {
if !output.is_empty() {
output.push('\n');
}
output.push_str(&format!(
" - unknown flags: {:#018x}",
self.unknown_flags.bits()
));
}
write!(f, "{}", output)
}
}
}
}
/// Converts a reference to `ControlFlagsModel` to `EffectiveControlFlags` using default flags.
///
/// This implementation allows conversion without consuming the model.
///
/// # Examples
///
/// ```rust
/// use pvimg::uvdata::{EffectiveControlFlags, SeHdrControlFlagsModel, SeTarget};
///
/// let pcf_v1 = SeHdrControlFlagsModel::pcf_for_target(SeTarget::V1Max);
/// let flags: EffectiveControlFlags<_> = (&pcf_v1).into();
/// // pcf_v1 is still usable here
/// ```
impl<T: ControlFlagTrait> From<&ControlFlagsModel<T>> for EffectiveControlFlags<T> {
fn from(model: &ControlFlagsModel<T>) -> Self {
EffectiveControlFlags {
version: model.version(),
known_flags: model.default.clone(),
unknown_flags: UnknownFlags::empty(),
}
}
}
/// Converts `ControlFlagsModel` to `Msb0Flags64` using default flags.
///
/// This consumes the model and converts it to a bitfield representation.
impl<T: ControlFlagTrait> From<ControlFlagsModel<T>> for Msb0Flags64 {
fn from(model: ControlFlagsModel<T>) -> Self {
let mut value = Msb0Flags64::default();
for flag in &model.default {
value.set_bit(flag.bit_position());
}
value
}
}
/// Converts a reference to `ControlFlagsModel` to `Msb0Flags64` using default flags.
///
/// This implementation allows conversion without consuming the model.
impl<T: ControlFlagTrait> From<&ControlFlagsModel<T>> for Msb0Flags64 {
fn from(model: &ControlFlagsModel<T>) -> Self {
let mut value = Msb0Flags64::default();
for flag in &model.default {
value.set_bit(flag.bit_position());
}
value
}
}
/// Display implementation for `ControlFlagsModel<T>`.
///
/// Provides two display formats:
/// - Normal format: Lists all default (enabled) flags with " - " prefix, one per line
/// - Alternate format (`{:#}`): Shows the raw bitfield as 66-character binary string
///
/// Note: This displays only the flags in the model's `default` set. Unknown flags
/// are not tracked by `ControlFlagsModel` - they are returned separately by
/// `from_msb0_flags()` when parsing raw bitfields.
///
/// # Examples
///
/// ```rust
/// use pvimg::uvdata::{SeHdrControlFlagsModel, SeTarget};
///
/// let pcf_v1 = SeHdrControlFlagsModel::pcf_for_target(SeTarget::V1Max);
///
/// // Normal format - lists default flags
/// println!("{}", pcf_v1);
///
/// // Alternate format - shows binary representation
/// println!("{:#}", pcf_v1);
/// ```
impl<T: ControlFlagTrait> Display for ControlFlagsModel<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if f.alternate() {
// Alternate format: show as binary
let flags: Msb0Flags64 = self.into();
write!(f, "{:#066b}", <u64>::from(flags))
} else {
// Normal format: list default (enabled) flags
let flags_s: Vec<String> = self
.default
.iter()
.map(|flag| format!(" - {flag}"))
.collect();
if flags_s.is_empty() {
write!(f, "(no flags enabled)")
} else {
write!(f, "{}", flags_s.join("\n"))
}
}
}
}
/// Error type for `TryFrom<Msb0Flags64>` conversion.
#[derive(Debug)]
// Will be used in an upcoming commit
#[expect(dead_code)]
pub enum Msb0FlagsConversionError<T: ControlFlagTrait> {
/// A flag bit is set that is not supported in any version
UnsupportedFlag { bit_position: u8 },
/// A flag bit is set that is not supported in the specified version
NotSupportedInVersion { flag: T, version: SeHdrVersion },
}
impl<T: ControlFlagTrait> Display for Msb0FlagsConversionError<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnsupportedFlag { bit_position } => {
write!(f, "Unsupported flag at bit position {}", bit_position)
}
Self::NotSupportedInVersion { flag, version } => {
write!(f, "Flag {} not supported in version {:?}", flag, version)
}
}
}
}
impl<T: ControlFlagTrait> std::error::Error for Msb0FlagsConversionError<T> {}
/// Override configuration for control flags.
///
/// Allows specifying individual flag states that override the default configuration.
/// This is useful for customizing flag settings on a per-flag basis.
///
/// # Type Parameters
///
/// * `T` - The control flag enum type (e.g., [`Flag`])
///
/// # Examples
///
/// ```rust
/// use pvimg::uvdata::{FlagsOverride, SeHdrFlag};
///
/// let mut overrides = FlagsOverride::new();
/// overrides.enable(SeHdrFlag::ConfidentialDump);
/// overrides.disable(SeHdrFlag::PckmoAes);
/// ```
#[derive(Debug, Clone)]
pub struct FlagsOverride<T: ControlFlagTrait> {
overrides: HashMap<T, FlagState>,
}
impl<T: ControlFlagTrait> FlagsOverride<T> {
/// Creates a new empty FlagsOverride.
pub fn new() -> Self {
Self {
overrides: HashMap::new(),
}
}
/// Sets an override for a specific flag.
///
/// # Arguments
///
/// * `flag` - The flag to override
/// * `state` - The desired state (Enabled or Disabled)
pub(super) fn set(&mut self, flag: T, state: FlagState) {
self.overrides.insert(flag, state);
}
/// Enables a specific flag by setting its override state to Enabled.
///
/// This is a convenience method equivalent to `set(flag, FlagState::Enabled)`.
///
/// # Arguments
///
/// * `flag` - The flag to enable
///
/// # Examples
///
/// ```rust
/// use pvimg::uvdata::{FlagsOverride, SeHdrFlag};
///
/// let mut overrides = FlagsOverride::new();
/// overrides.enable(SeHdrFlag::ConfidentialDump);
/// ```
pub fn enable(&mut self, flag: T) {
self.set(flag, FlagState::Enabled);
}
/// Disables a specific flag by setting its override state to Disabled.
///
/// This is a convenience method equivalent to `set(flag, FlagState::Disabled)`.
///
/// # Arguments
///
/// * `flag` - The flag to disable
///
/// # Examples
///
/// ```rust
/// use pvimg::uvdata::{FlagsOverride, SeHdrFlag};
///
/// let mut overrides = FlagsOverride::new();
/// overrides.disable(SeHdrFlag::PckmoAes);
/// ```
pub fn disable(&mut self, flag: T) {
self.set(flag, FlagState::Disabled);
}
/// Enables multiple flags at once from an iterator.
///
/// This is a convenience method for enabling multiple flags in a single call.
///
/// # Arguments
///
/// * `flags` - An iterator over flags to enable
///
/// # Examples
///
/// ```rust
/// use pvimg::uvdata::{FlagsOverride, SeHdrFlag};
///
/// let mut overrides = FlagsOverride::new();
/// overrides.enable_all([SeHdrFlag::ConfidentialDump, SeHdrFlag::PckmoAes]);
/// ```
pub fn enable_all<I>(&mut self, flags: I)
where
I: IntoIterator<Item = T>,
{
for flag in flags {
self.enable(flag);
}
}
/// Disables multiple flags at once from an iterator.
///
/// This is a convenience method for disabling multiple flags in a single call.
///
/// # Arguments
///
/// * `flags` - An iterator over flags to disable
///
/// # Examples
///
/// ```rust
/// use pvimg::uvdata::{FlagsOverride, SeHdrFlag};
///
/// let mut overrides = FlagsOverride::new();
/// overrides.disable_all([SeHdrFlag::PckmoAes, SeHdrFlag::PckmoEcc]);
/// ```
pub fn disable_all<I>(&mut self, flags: I)
where
I: IntoIterator<Item = T>,
{
for flag in flags {
self.disable(flag);
}
}
/// Removes an override for a specific flag.
///
/// # Arguments
///
/// * `flag` - The flag to remove the override for
///
/// # Returns
///
/// The previous state if it existed, None otherwise
pub fn remove(&mut self, flag: T) -> Option<FlagState> {
self.overrides.remove(&flag)
}
/// Gets the override state for a specific flag.
///
/// # Arguments
///
/// * `flag` - The flag to query
///
/// # Returns
///
/// The override state if it exists, None otherwise
pub fn get(&self, flag: T) -> Option<FlagState> {
self.overrides.get(&flag).copied()
}
/// Checks if an override exists for a specific flag.
///
/// # Arguments
///
/// * `flag` - The flag to check
pub fn has_override(&self, flag: T) -> bool {
self.overrides.contains_key(&flag)
}
/// Returns an iterator over all overrides.
pub fn iter(&self) -> impl Iterator<Item = (&T, &FlagState)> {
self.overrides.iter()
}
/// Returns an iterator over all flags that have overrides.
pub fn flags(&self) -> impl Iterator<Item = &T> {
self.overrides.keys()
}
/// Returns the number of overrides.
pub fn len(&self) -> usize {
self.overrides.len()
}
/// Checks if there are no overrides.
#[must_use]
pub fn is_empty(&self) -> bool {
self.overrides.is_empty()
}
/// Clears all overrides.
pub fn clear(&mut self) {
self.overrides.clear();
}
}
impl<T: ControlFlagTrait> Default for FlagsOverride<T> {
fn default() -> Self {
Self::new()
}
}

View File

@@ -18,6 +18,7 @@ use serde::{Deserialize, Serialize};
use utils::HexSlice;
use super::keys::phkh_v1;
use super::{EffectiveControlFlags, SeHdrControlFlags};
use crate::error::Error;
use crate::misc::PAGESIZE;
use crate::pv_utils::error::Result;
@@ -37,9 +38,7 @@ use crate::pv_utils::uvdata::{
UvDataTrait,
};
use crate::pv_utils::uvdata_builder::{AeadCipherBuilderTrait, KeyExchangeBuilderTrait};
use crate::pv_utils::{
try_copy_slice_to_array, PlaintextControlFlagsV1, SecretControlFlagsV1, PSW,
};
use crate::pv_utils::{try_copy_slice_to_array, SeHdrFlag, SeTarget, PSW};
#[derive(Debug)]
struct HdrSizesV1 {
@@ -100,17 +99,15 @@ impl Display for SeHdrAadV1 {
writeln!(
f,
"plaintext control flags:\n{}",
PlaintextControlFlagsV1::from(self.pcf)
SeHdrControlFlags::from_u64(self.pcf, SeTarget::V1Max, true)
)?;
Ok(())
}
}
impl KeyExchangeTrait for SeHdrAadV1 {
fn contains<K: AsRef<PKeyRef<Public>>>(&self, key: K) -> Result<bool> {
let phkh = phkh_v1(key)?;
Ok(self.contains_hash(phkh))
}
type PrivateKeyType = PKey<Private>;
type TargetKeyType = PKeyRef<Public>;
fn cust_pub_key(&mut self) -> Result<PKey<Public>> {
self.cust_pub_key.clone().try_into()
@@ -129,6 +126,14 @@ impl KeyExchangeTrait for SeHdrAadV1 {
}
false
}
fn contains<K>(&self, key: K) -> Result<bool>
where
K: AsRef<Self::TargetKeyType>,
{
let phkh = phkh_v1(key)?;
Ok(self.contains_hash(phkh))
}
}
#[derive(PartialEq, Eq, Debug, Clone, DekuRead, DekuWrite, Serialize, Deserialize)]
@@ -176,7 +181,7 @@ impl Display for SeHdrConfV1 {
writeln!(
f,
"secret control flags:\n{}",
SecretControlFlagsV1::from(self.scf)
SeHdrControlFlags::from_u64(self.scf, SeTarget::V1Max, false)
)?;
// Support verbose mode if the `alternate` (`{:#}`) flag is used.
@@ -413,11 +418,15 @@ impl UvDataPlainTrait for SeHdrDataV1 {
impl SeHdrPlainTrait for SeHdrDataV1 {}
impl KeyExchangeBuilderTrait for SeHdrDataV1 {
type AeadKeyType = SymKey;
type PrivateKeyType = PKeyRef<Private>;
type TargetKeyType = PKeyRef<Public>;
fn add_keyslot(
&mut self,
hostkey: &PKeyRef<Public>,
aead_key: &SymKey,
priv_key: &PKeyRef<Private>,
hostkey: &Self::TargetKeyType,
aead_key: &Self::AeadKeyType,
priv_key: &Self::PrivateKeyType,
) -> Result<()> {
let keyslot = Keyslot::new(HostKey::V1(hostkey.to_owned()));
let keyslot_bin = keyslot.encrypt(aead_key.value(), priv_key)?.try_into()?;
@@ -461,9 +470,8 @@ impl KeyExchangeBuilderTrait for SeHdrDataV1 {
}
impl KeyExchangeTrait for SeHdrDataV1 {
fn contains<K: AsRef<PKeyRef<Public>>>(&self, key: K) -> Result<bool> {
self.aad.contains(key)
}
type PrivateKeyType = PKey<Private>;
type TargetKeyType = PKeyRef<Public>;
fn cust_pub_key(&mut self) -> Result<PKey<Public>> {
self.aad.cust_pub_key()
@@ -476,6 +484,13 @@ impl KeyExchangeTrait for SeHdrDataV1 {
fn contains_hash<H: AsRef<[u8]>>(&self, hash: H) -> bool {
self.aad.contains_hash(hash)
}
fn contains<K>(&self, key: K) -> Result<bool>
where
K: AsRef<Self::TargetKeyType>,
{
self.aad.contains(key)
}
}
impl SeHdrConfBuilderTrait for SeHdrDataV1 {
@@ -483,8 +498,8 @@ impl SeHdrConfBuilderTrait for SeHdrDataV1 {
self.data.value_mut().psw = psw.clone();
}
fn set_scf(&mut self, scf: &SecretControlFlagsV1) -> Result<()> {
self.data.value_mut().scf = scf.into();
fn set_scf(&mut self, scf: &EffectiveControlFlags<SeHdrFlag>) -> Result<()> {
self.data.value_mut().scf = scf.to_u64();
Ok(())
}
@@ -499,8 +514,8 @@ impl SeHdrConfBuilderTrait for SeHdrDataV1 {
}
impl SeHdrPubBuilderTrait for SeHdrDataV1 {
fn set_pcf(&mut self, pcf: &PlaintextControlFlagsV1) -> Result<()> {
self.aad.pcf = pcf.into();
fn set_pcf(&mut self, pcf: &EffectiveControlFlags<SeHdrFlag>) -> Result<()> {
self.aad.pcf = pcf.to_u64();
Ok(())
}
@@ -591,9 +606,8 @@ impl AeadCipherBuilderTrait for SeHdrDataV1 {
}
impl KeyExchangeTrait for SeHdrBinV1 {
fn contains<K: AsRef<PKeyRef<Public>>>(&self, key: K) -> Result<bool> {
self.aad.contains(key)
}
type PrivateKeyType = PKey<Private>;
type TargetKeyType = PKeyRef<Public>;
fn cust_pub_key(&mut self) -> Result<PKey<Public>> {
self.aad.cust_pub_key()
@@ -606,6 +620,13 @@ impl KeyExchangeTrait for SeHdrBinV1 {
fn contains_hash<H: AsRef<[u8]>>(&self, hash: H) -> bool {
self.aad.contains_hash(hash)
}
fn contains<K>(&self, key: K) -> Result<bool>
where
K: AsRef<Self::TargetKeyType>,
{
self.aad.contains(key)
}
}
impl AeadDataTrait for SeHdrBinV1 {
@@ -655,6 +676,7 @@ mod tests {
use std::io::Cursor;
use pv::request::HostKey;
use pv::test_utils::get_test_key_and_cert;
use super::*;
@@ -663,7 +685,7 @@ mod tests {
#[test]
fn iv_keys_auto_generation_test() {
let (_, host_key) = get_test_key_and_cert();
let host_keys = [host_key.public_key().unwrap()];
let host_keys = [HostKey::V1(host_key.public_key().unwrap())];
let mut builder = SeHdrBuilder::new(
SeHdrVersion::V1,
PSW {
@@ -685,7 +707,7 @@ mod tests {
#[test]
fn chain_test() {
let (_, host_key) = get_test_key_and_cert();
let host_keys = [host_key.public_key().unwrap()];
let host_keys = [HostKey::V1(host_key.public_key().unwrap())];
let xts_key = Confidential::new([0x3; SymKeyType::AES_256_XTS_KEY_LEN]);
let meta = ComponentMetadataV1 {
ald: [0x1; SHA_512_HASH_LEN],
@@ -731,7 +753,7 @@ mod tests {
const MAX_HOST_KEYS: usize = 95;
let (_, host_key) = get_test_key_and_cert();
let pub_key = host_key.public_key().unwrap();
let pub_key = HostKey::V1(host_key.public_key().unwrap());
let host_keys_max: Vec<_> = (0..MAX_HOST_KEYS).map(|_| pub_key.clone()).collect();
let too_many_host_keys: Vec<_> = (0..MAX_HOST_KEYS + 1).map(|_| pub_key.clone()).collect();
let xts_key = Confidential::new([0x3; SymKeyType::AES_256_XTS_KEY_LEN]);

View File

@@ -0,0 +1,916 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2026
use std::fmt::Display;
use std::mem::{size_of, size_of_val};
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use deku::ctx::Endian;
use deku::prelude::*;
use openssl::nid::Nid;
use openssl::pkey::{PKeyRef, Public};
use pv::request::openssl::pkey::{PKey, Private};
use pv::request::{
gen_ec_key, random_array, Aes256XtsKey, Confidential, EcPubKeyCoord, HybridPKey, KeyslotV2,
SymKey, SymKeyType, Zeroize, SHA_512_HASH_LEN,
};
use serde::{Deserialize, Serialize};
use utils::HexSlice;
use super::keys::phkh_v2;
use super::{EffectiveControlFlags, SeHdrControlFlags};
use crate::error::Error;
use crate::pv_utils::error::Result;
use crate::pv_utils::misc::display_indented;
use crate::pv_utils::se_hdr::brb::{
ComponentMetadata, ComponentMetadataV1, SeHdrCommon, SeHdrConfBuilderTrait, SeHdrPlainTrait,
SeHdrPubBuilderTrait, SeHdrTrait,
};
use crate::pv_utils::se_hdr::keys::{BinaryKeySlotV2, EcPubKeyCoordV1};
use crate::pv_utils::serializing::{
bytesize, bytesize_confidential, confidential_read_slice, confidential_write_slice,
serde_base64, serde_hex_array, serde_hex_confidential_array, serde_hex_left_padded_u64,
serialize_to_bytes,
};
use crate::pv_utils::uv_keys::UvKeyHashV1;
use crate::pv_utils::uvdata::{
AeadCipherTrait, AeadDataTrait, AeadPlainDataTrait, KeyExchangeTrait, UvDataPlainTrait,
UvDataTrait,
};
use crate::pv_utils::uvdata_builder::{AeadCipherBuilderTrait, KeyExchangeBuilderTrait};
use crate::pv_utils::{try_copy_slice_to_array, SeHdrFlag, SeTarget, PSW};
#[derive(Debug)]
struct HdrSizesV2 {
pub phs: u64,
pub sea: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, DekuRead, DekuWrite, Serialize, Deserialize)]
#[deku(endian = "endian", ctx = "endian: Endian", ctx_default = "Endian::Big")]
pub struct SeHdrAadV2 {
#[deku(assert = "*sehs <= SeHdrDataV2::MAX_SIZE.try_into().unwrap()")]
pub sehs: u32,
#[serde(with = "serde_hex_array", rename = "iv_hex")]
pub iv: [u8; SymKeyType::AES_256_GCM_IV_LEN],
#[serde(skip)]
res1: u32,
#[deku(assert = "*nks <= (*sehs).into()", update = "self.keyslots.len()")]
pub nks: u64,
#[deku(assert = "*sea <= (*sehs).into()")]
pub sea: u64,
pub nep: u64,
#[serde(with = "serde_hex_left_padded_u64", rename = "pcf_hex")]
pub pcf: u64,
pub cust_pub_key: EcPubKeyCoordV1,
#[serde(with = "serde_hex_array", rename = "pld_hex")]
pub pld: [u8; SHA_512_HASH_LEN],
#[serde(with = "serde_hex_array", rename = "ald_hex")]
pub ald: [u8; SHA_512_HASH_LEN],
#[serde(with = "serde_hex_array", rename = "tld_hex")]
pub tld: [u8; SHA_512_HASH_LEN],
#[deku(count = "nks")]
pub keyslots: Vec<BinaryKeySlotV2>,
}
impl SeHdrAadV2 {
const KEY_TYPE: SymKeyType = SymKeyType::Aes256Gcm;
}
impl Display for SeHdrAadV2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Support verbose mode if the `alternate` (`{:#}`) flag is used.
if f.alternate() {
writeln!(f, "size: {} bytes", self.sehs)?;
writeln!(f, "number of key slots: {}", self.nks)?;
}
writeln!(f, "key slots:")?;
for s in &self.keyslots {
writeln!(f, " - {s}")?;
}
if f.alternate() {
let value = display_indented(f, &self.cust_pub_key, 2);
writeln!(f, "customer public key:\n{value}",)?;
writeln!(f, "number of component pages: {}", self.nep)?;
writeln!(f, "components content hash: {:}", HexSlice::from(&self.pld))?;
writeln!(f, "components address hash: {:}", HexSlice::from(&self.ald))?;
writeln!(f, "components tweak hash: {:}", HexSlice::from(&self.tld))?;
}
writeln!(
f,
"plaintext control flags:\n{}",
SeHdrControlFlags::from_u64(self.pcf, SeTarget::V2Max, true)
)?;
Ok(())
}
}
impl KeyExchangeTrait for SeHdrAadV2 {
type PrivateKeyType = PKey<Private>;
type TargetKeyType = HybridPKey;
fn cust_pub_key(&mut self) -> Result<PKey<Public>> {
self.cust_pub_key.clone().try_into()
}
fn key_type(&self) -> SymKeyType {
Self::KEY_TYPE
}
/** contains_hash - test if the given hash is in this SE-header
*
* * `hash`: hash to compare, either 32 or 64 byte long
*
* Returns:
* - false if the given hash is not 64 or 32 bytes long
* - the comparison result otherwise
*/
fn contains_hash<H: AsRef<[u8]>>(&self, hash: H) -> bool {
let hash = hash.as_ref();
let size = hash.len();
match size {
UvKeyHashV1::UV_KEY_HASH_SIZE | SHA_512_HASH_LEN => (),
_ => return false,
};
self.keyslots
.iter()
.any(|ks| ks.phkh[..size] == hash[..size])
}
fn contains<K>(&self, key: K) -> Result<bool>
where
K: AsRef<Self::TargetKeyType>,
{
let key = key.as_ref();
let phkh = phkh_v2(key)?;
Ok(self.contains_hash(phkh))
}
}
#[derive(PartialEq, Eq, Debug, Clone, DekuRead, DekuWrite, Serialize, Deserialize)]
#[deku(endian = "endian", ctx = "endian: Endian", ctx_default = "Endian::Big")]
pub struct SeHdrConfV2 {
#[serde(with = "serde_hex_confidential_array", rename = "cck_hex")]
#[deku(
reader = "confidential_read_slice(deku::reader, endian)",
writer = "confidential_write_slice(cck, deku::writer, endian)"
)]
cck: Confidential<[u8; 32]>,
#[serde(with = "serde_hex_confidential_array", rename = "xts_hex")]
#[deku(
reader = "confidential_read_slice(deku::reader, endian)",
writer = "confidential_write_slice(xts, deku::writer, endian)"
)]
xts: Aes256XtsKey,
psw: PSW,
#[serde(with = "serde_hex_left_padded_u64", rename = "scf_hex")]
pub scf: u64,
#[serde(skip)]
#[deku(assert_eq = "0")]
noi: u32,
#[serde(skip)]
res2: u32,
#[serde(skip)]
#[deku(count = "noi")]
opt_items: Vec<u8>,
}
impl Zeroize for SeHdrConfV2 {
fn zeroize(&mut self) {
self.cck.zeroize();
self.xts.zeroize();
self.psw.zeroize();
self.scf.zeroize();
self.noi.zeroize();
self.res2.zeroize();
self.opt_items.zeroize();
}
}
impl Display for SeHdrConfV2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(
f,
"secret control flags:\n{}",
SeHdrControlFlags::from_u64(self.scf, SeTarget::V2Max, false)
)?;
// Support verbose mode if the `alternate` (`{:#}`) flag is used.
if f.alternate() {
writeln!(f, "CCK: {:}", HexSlice::from(self.cck.value()))?;
writeln!(f, "XTS key: {:}", HexSlice::from(self.xts.value()))?;
let psw = display_indented(f, &self.psw, 2);
writeln!(f, "PSW:\n{psw}")?;
}
Ok(())
}
}
#[derive(Default, PartialEq, Eq, Debug, Clone, DekuRead, DekuWrite, Serialize, Deserialize)]
#[deku(endian = "endian", ctx = "endian: Endian", ctx_default = "Endian::Big")]
pub struct SeHdrTagV2 {
#[serde(with = "serde_hex_array", rename = "tag_hex")]
tag: [u8; SymKeyType::AES_256_GCM_TAG_LEN],
}
impl Display for SeHdrTagV2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:}", HexSlice::from(&self.tag))
}
}
mod ser_confidential_confv2 {
use pv::request::Confidential;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::SeHdrConfV2;
pub fn serialize<S: Serializer>(
encrypted: &Confidential<SeHdrConfV2>,
ser: S,
) -> Result<S::Ok, S::Error> {
encrypted.value().serialize(ser)
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Confidential<SeHdrConfV2>, D::Error> {
let conf = SeHdrConfV2::deserialize(deserializer)?;
Ok(Confidential::new(conf))
}
}
/// Secure Execution Header definition
#[derive(Debug, Clone, PartialEq, Eq, DekuRead, DekuWrite, Serialize, Deserialize)]
#[deku(endian = "big")]
pub struct SeHdrDataV2 {
#[serde(flatten)]
pub aad: SeHdrAadV2,
#[serde(flatten, with = "ser_confidential_confv2")]
#[deku(
reader = "confidential_read_sehdrconf_v2(deku::reader)",
writer = "confidential_write_sehdrconf_v2(data, deku::writer)"
)]
pub data: Confidential<SeHdrConfV2>,
#[serde(flatten)]
tag: SeHdrTagV2,
}
impl Display for SeHdrDataV2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Support verbose mode if the `alternate` (`{:#}`) flag is used.
if f.alternate() {
write!(f, "{:#}", self.aad)?;
write!(f, "{:#}", self.data.value())?;
writeln!(f, "GCM tag: {}", self.tag)?;
} else {
write!(f, "{}", self.aad)?;
write!(f, "{}", self.data.value())?;
}
Ok(())
}
}
/// Reads from a `reader` and creates a confidential `SeHdrConfV2`.
///
/// # Errors
///
/// This function will return an error if there was an I/O error or the
/// `SeHdrConfV2` could not be constructed.
fn confidential_read_sehdrconf_v2<R>(
reader: &mut Reader<R>,
) -> Result<Confidential<SeHdrConfV2>, DekuError>
where
R: std::io::Read + std::io::Seek,
{
Ok(Confidential::new(SeHdrConfV2::from_reader_with_ctx(
reader,
(),
)?))
}
/// Writes a `Confidential<SeHdrConf1>` into this `writer`.
///
/// # Errors
///
/// This function will return an error if there was an I/O error.
fn confidential_write_sehdrconf_v2<W>(
value: &Confidential<SeHdrConfV2>,
writer: &mut Writer<W>,
) -> Result<(), DekuError>
where
W: std::io::Write + std::io::Seek,
{
value.value().to_writer(writer, ())
}
impl SeHdrDataV2 {
// For Linux kernel >= 7.0, this is 1 MiB
const MAX_SIZE: usize = 1024 * 1024;
const PCF_DEFAULT: u64 = 0x0;
const SCF_DEFAULT: u64 = 0x0;
/// Creates a new `SeHdrDataV2`. It initializes the CCK and IV with random
/// data.
///
/// # Errors
///
/// This function will return an error if there was not enough entropy to
/// create the random data or another error has occurred.
pub fn new(psw: PSW, components: ComponentMetadataV1) -> Result<Self> {
// Safety: The CCK is also 32 bytes large.
let cck = SymKey::random(SymKeyType::Aes256Gcm)?.try_into().unwrap();
let mut ret = Self {
aad: SeHdrAadV2 {
sehs: 0,
pcf: Self::PCF_DEFAULT,
ald: components.ald,
pld: components.pld,
tld: components.tld,
nep: components.nep,
sea: 0,
iv: random_array()?,
res1: 0,
nks: 0,
cust_pub_key: EcPubKeyCoordV1 { coord: [0_u8; 160] },
keyslots: vec![],
},
data: SeHdrConfV2 {
cck,
scf: Self::SCF_DEFAULT,
psw,
xts: components.key,
noi: 0,
res2: 0,
opt_items: vec![],
}
.into(),
tag: SeHdrTagV2::default(),
};
let hdr_size = ret.size()?;
let phs = hdr_size.phs.try_into()?;
if phs > Self::MAX_SIZE {
return Err(Error::InvalidSeHdrTooLarge {
given: phs,
maximum: Self::MAX_SIZE,
});
}
ret.aad.sehs = phs.try_into()?;
ret.aad.sea = hdr_size.sea;
Ok(ret)
}
fn size(&self) -> Result<HdrSizesV2> {
let sea = bytesize_confidential(&self.data)?;
let mut phs = bytesize(&self.aad)?
.checked_add(size_of::<SeHdrCommon>())
.ok_or(Error::UnexpectedOverflow)?;
phs = phs
.checked_add(bytesize(&self.tag)?)
.ok_or(Error::UnexpectedOverflow)?;
phs = phs.checked_add(sea).ok_or(Error::UnexpectedOverflow)?;
Ok(HdrSizesV2 {
sea: sea.try_into()?,
phs: phs.try_into()?,
})
}
/// Return the expected size of an constructed `SeHdrDataV2` with `n` key
/// slots.
///
/// # Errors
///
/// This function will return an error if there was an arithmetic overflow
/// or.
pub fn expected_size(nks: usize) -> Result<usize> {
let cck = [0x0; 32].into();
let hdr = Self {
aad: SeHdrAadV2 {
sehs: 0,
pcf: Self::PCF_DEFAULT,
ald: [0x0; SHA_512_HASH_LEN],
pld: [0x0; SHA_512_HASH_LEN],
tld: [0x0; SHA_512_HASH_LEN],
nep: 0,
sea: 0,
iv: [0x0_u8; SymKeyType::AES_256_GCM_IV_LEN],
res1: 0,
nks: 0,
cust_pub_key: EcPubKeyCoordV1 { coord: [0_u8; 160] },
keyslots: vec![],
},
data: SeHdrConfV2 {
cck,
scf: Self::SCF_DEFAULT,
psw: PSW { mask: 0, addr: 0 },
xts: [0x0; SymKeyType::AES_256_XTS_KEY_LEN].into(),
noi: 0,
res2: 0,
opt_items: vec![],
}
.into(),
tag: SeHdrTagV2::default(),
};
let hdr_size: usize = hdr.size()?.phs.try_into().unwrap();
hdr_size
.checked_add(
size_of::<BinaryKeySlotV2>()
.checked_mul(nks)
.ok_or(Error::UnexpectedOverflow)?,
)
.ok_or(Error::UnexpectedOverflow)
}
}
impl UvDataPlainTrait for SeHdrDataV2 {
type C = SeHdrBinV2;
}
impl SeHdrPlainTrait for SeHdrDataV2 {}
impl KeyExchangeBuilderTrait for SeHdrDataV2 {
type AeadKeyType = SymKey;
type PrivateKeyType = PKeyRef<Private>;
type TargetKeyType = HybridPKey;
fn generate_private_key(&self) -> Result<PKey<Private>> {
Ok(gen_ec_key(Nid::SECP521R1)?)
}
fn set_cust_public_key(&mut self, key: &PKeyRef<Private>) -> Result<()> {
self.aad.cust_pub_key = TryInto::<EcPubKeyCoord>::try_into(key)?.into();
Ok(())
}
fn clear_keyslots(&mut self) -> Result<()> {
let old_nks: usize = self.aad.nks.try_into().unwrap();
let keyslot_bin_size = size_of::<BinaryKeySlotV2>();
self.aad.keyslots.clear();
self.aad.nks = 0;
self.aad.sehs -= u32::try_from(
old_nks
.checked_mul(keyslot_bin_size)
.ok_or(Error::UnexpectedOverflow)?,
)
.unwrap();
Ok(())
}
fn add_keyslot(
&mut self,
hostkey: &Self::TargetKeyType,
aead_key: &Self::AeadKeyType,
priv_key: &Self::PrivateKeyType,
) -> Result<()> {
let keyslot = KeyslotV2::new(hostkey.clone());
let keyslot_bin = keyslot.encrypt(aead_key.value(), priv_key)?.try_into()?;
let keyslot_bin_size = u32::try_from(size_of_val(&keyslot_bin)).unwrap();
self.aad.keyslots.push(keyslot_bin);
self.aad.nks = self
.aad
.nks
.checked_add(1)
.ok_or(Error::UnexpectedOverflow)?;
self.aad.sehs = self
.aad
.sehs
.checked_add(keyslot_bin_size)
.ok_or(Error::UnexpectedOverflow)?;
Ok(())
}
}
impl KeyExchangeTrait for SeHdrDataV2 {
type PrivateKeyType = PKey<Private>;
type TargetKeyType = HybridPKey;
fn cust_pub_key(&mut self) -> Result<PKey<Public>> {
self.aad.cust_pub_key()
}
fn key_type(&self) -> SymKeyType {
self.aad.key_type()
}
fn contains_hash<H: AsRef<[u8]>>(&self, hash: H) -> bool {
self.aad.contains_hash(hash)
}
fn contains<K>(&self, key: K) -> Result<bool>
where
K: AsRef<Self::TargetKeyType>,
{
self.aad.contains(key)
}
}
impl SeHdrConfBuilderTrait for SeHdrDataV2 {
fn set_psw(&mut self, psw: &PSW) {
self.data.value_mut().psw = psw.clone();
}
fn set_scf(&mut self, scf: &EffectiveControlFlags<SeHdrFlag>) -> Result<()> {
self.data.value_mut().scf = scf.to_u64();
Ok(())
}
fn set_cck(&mut self, cck: Confidential<Vec<u8>>) -> Result<()> {
self.data.value_mut().cck = cck.try_into()?;
Ok(())
}
fn generate_cck(&self) -> Result<SymKey> {
Ok(SymKey::random(SymKeyType::Aes256Gcm)?)
}
}
impl SeHdrPubBuilderTrait for SeHdrDataV2 {
fn set_pcf(&mut self, pcf: &EffectiveControlFlags<SeHdrFlag>) -> Result<()> {
self.aad.pcf = pcf.to_u64();
Ok(())
}
fn set_components(&mut self, meta: ComponentMetadata) -> Result<()> {
let ComponentMetadataV1 {
ald,
pld,
tld,
nep,
key,
}: ComponentMetadataV1 = meta
.try_into()
.map_err(|_| Error::InvalidComponentMetadata)?;
self.data.value_mut().xts = key;
self.aad.ald = ald;
self.aad.pld = pld;
self.aad.tld = tld;
self.aad.nep = nep;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, DekuRead, DekuWrite, Serialize, Deserialize)]
#[deku(endian = "big")]
pub struct SeHdrBinV2 {
#[serde(flatten)]
pub aad: SeHdrAadV2,
#[serde(with = "serde_base64", rename = "cipher_data_b64")]
#[deku(bytes_read = "aad.sea")]
pub data: Vec<u8>,
#[serde(flatten)]
pub tag: SeHdrTagV2,
}
impl Display for SeHdrBinV2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Support verbose mode if the `alternate` (`{:#}`) flag is used.
if f.alternate() {
write!(f, "{:#}", self.aad)?;
writeln!(
f,
"encrypted data: {:#}",
BASE64_STANDARD.encode(&self.data)
)?;
writeln!(f, "GCM tag: {:#}", self.tag)?;
} else {
write!(f, "{}", self.aad)?;
}
Ok(())
}
}
impl SeHdrBinV2 {
pub fn new(d: &[u8]) -> Result<Self> {
Self::try_from_data(d)
}
pub(crate) fn try_from_data(data: &[u8]) -> Result<Self> {
let (_rest, val) = Self::from_bytes((data, 0))?;
Ok(val)
}
}
impl UvDataTrait for SeHdrBinV2 {
type P = SeHdrDataV2;
}
impl SeHdrTrait for SeHdrBinV2 {}
impl AeadCipherTrait for SeHdrBinV2 {
fn aead_key_type(&self) -> SymKeyType {
self.key_type()
}
fn iv(&self) -> &[u8] {
&self.aad.iv
}
fn aead_tag_size(&self) -> usize {
SymKeyType::AES_256_GCM_TAG_LEN
}
}
impl AeadCipherBuilderTrait for SeHdrDataV2 {
fn set_iv(&mut self, iv: &[u8]) -> Result<()> {
self.aad.iv = try_copy_slice_to_array(iv)?;
Ok(())
}
}
impl KeyExchangeTrait for SeHdrBinV2 {
type PrivateKeyType = PKey<Private>;
type TargetKeyType = HybridPKey;
fn cust_pub_key(&mut self) -> Result<PKey<Public>> {
self.aad.cust_pub_key()
}
fn key_type(&self) -> SymKeyType {
self.aad.key_type()
}
fn contains_hash<H: AsRef<[u8]>>(&self, hash: H) -> bool {
self.aad.contains_hash(hash)
}
fn contains<K>(&self, key: K) -> Result<bool>
where
K: AsRef<Self::TargetKeyType>,
{
self.aad.contains(key)
}
}
impl AeadDataTrait for SeHdrBinV2 {
fn aad(&self) -> Result<Vec<u8>> {
serialize_to_bytes(&self.aad)
}
fn data(&self) -> Vec<u8> {
self.data.to_owned()
}
fn tag(&self) -> Vec<u8> {
serialize_to_bytes(&self.tag).unwrap()
}
}
impl AeadPlainDataTrait for SeHdrDataV2 {
fn aad(&self) -> Result<Vec<u8>> {
serialize_to_bytes(&self.aad)
}
fn data(&self) -> Result<Confidential<Vec<u8>>> {
Ok(serialize_to_bytes(self.data.value())?.into())
}
fn tag(&self) -> Vec<u8> {
serialize_to_bytes(&self.tag).unwrap()
}
}
impl AeadCipherTrait for SeHdrDataV2 {
fn aead_key_type(&self) -> SymKeyType {
self.aad.key_type()
}
fn iv(&self) -> &[u8] {
&self.aad.iv
}
fn aead_tag_size(&self) -> usize {
SymKeyType::AES_256_GCM_TAG_LEN
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use pv::request::HostKey;
use pv::test_utils::get_test_key_and_cert_hybrid;
use super::*;
use crate::pv_utils::{BuilderTrait, SeHdr, SeHdrBuilder, SeHdrVersion};
#[test]
fn iv_keys_auto_generation_test() {
let (_, host_key1, host_key2) = get_test_key_and_cert_hybrid();
let host_keys = [HostKey::V2(
HybridPKey::new(
host_key1.public_key().unwrap(),
host_key2.public_key().unwrap(),
)
.unwrap(),
)];
let mut builder = SeHdrBuilder::new(
SeHdrVersion::V2,
PSW {
addr: 1234,
mask: 5678,
},
ComponentMetadataV1 {
ald: [0x1; SHA_512_HASH_LEN],
pld: [0x2; SHA_512_HASH_LEN],
tld: [0x3; SHA_512_HASH_LEN],
nep: 1,
key: Confidential::new([0x0_u8; SymKeyType::AES_256_XTS_KEY_LEN]),
},
)
.expect("should not fail");
builder.add_hostkeys(&host_keys).expect("should not fail");
}
#[test]
fn chain_test() {
let (_, host_key1, host_key2) = get_test_key_and_cert_hybrid();
let host_keys = [HostKey::V2(
HybridPKey::new(
host_key1.public_key().unwrap(),
host_key2.public_key().unwrap(),
)
.unwrap(),
)];
let xts_key = Confidential::new([0x3; SymKeyType::AES_256_XTS_KEY_LEN]);
let meta = ComponentMetadataV1 {
ald: [0x1; SHA_512_HASH_LEN],
pld: [0x2; SHA_512_HASH_LEN],
tld: [0x3; SHA_512_HASH_LEN],
nep: 3,
key: xts_key,
};
let cck: Confidential<Vec<u8>> = [0x42; 32].to_vec().into();
let psw = PSW {
addr: 1234,
mask: 5678,
};
let mut builder = SeHdrBuilder::new(SeHdrVersion::V2, psw.clone(), meta.clone())
.expect("should not fail");
builder
.add_hostkeys(&host_keys)
.expect("should not fail")
.with_components(meta.clone())
.expect("should not fail")
.with_cck(cck.clone())
.expect("should not fail");
let prot_key = builder.prot_key().to_owned();
let bin = builder.build().expect("should not fail");
let reader = Cursor::new(bin.as_bytes().expect("should not fail"));
let hdr = SeHdr::try_from_io(reader).unwrap();
let hdr_plain = hdr.decrypt(&prot_key).unwrap();
assert_eq!(hdr_plain.common.version, SeHdrVersion::V2);
let hdr_data_v2: SeHdrDataV2 = hdr_plain.data.try_into().expect("should not fail");
assert_eq!(meta.ald, hdr_data_v2.aad.ald);
assert_eq!(meta.pld, hdr_data_v2.aad.pld);
assert_eq!(meta.tld, hdr_data_v2.aad.tld);
assert_eq!(psw, hdr_data_v2.data.value().psw);
assert_eq!(cck.value(), hdr_data_v2.data.value().cck.value());
}
#[test]
fn max_size_sehdr_test() {
const MAX_HOST_KEYS: usize = 623; // since Linux kernel 7.0
let (_, host_key1, host_key2) = get_test_key_and_cert_hybrid();
let pub_key = HostKey::V2(
HybridPKey::new(
host_key1.public_key().unwrap(),
host_key2.public_key().unwrap(),
)
.unwrap(),
);
let host_keys_max: Vec<_> = (0..MAX_HOST_KEYS).map(|_| pub_key.clone()).collect();
let too_many_host_keys: Vec<_> = (0..MAX_HOST_KEYS + 1).map(|_| pub_key.clone()).collect();
let xts_key = Confidential::new([0x3; SymKeyType::AES_256_XTS_KEY_LEN]);
let meta = ComponentMetadataV1 {
ald: [0x1; SHA_512_HASH_LEN],
pld: [0x2; SHA_512_HASH_LEN],
tld: [0x3; SHA_512_HASH_LEN],
nep: 3,
key: xts_key,
};
let psw = PSW {
addr: 1234,
mask: 5678,
};
let mut builder = SeHdrBuilder::new(SeHdrVersion::V2, psw.clone(), meta.clone())
.expect("should not fail");
builder
.add_hostkeys(&host_keys_max)
.expect("should not fail")
.with_components(meta.clone())
.expect("should not fail");
let bin = builder.build().expect("should not fail");
assert_eq!(bin.common.version, SeHdrVersion::V2);
let hdr_v2: SeHdrBinV2 = bin.data.try_into().expect("should not fail");
assert_eq!(hdr_v2.aad.sehs, 1047200); // since kernel 7.0
let mut builder = SeHdrBuilder::new(SeHdrVersion::V2, psw.clone(), meta.clone())
.expect("should not fail");
builder
.add_hostkeys(&too_many_host_keys)
.expect("should not fail")
.with_components(meta)
.expect("should not fail");
assert!(matches!(builder.build(), Err(Error::InvalidSeHdr)));
}
#[test]
fn roundtrip_se_hdr_tag_v2_json() {
let tag = SeHdrTagV2 {
tag: [0x42; SymKeyType::AES_256_GCM_TAG_LEN],
};
let json = serde_json::to_string(&tag).expect("should serialize");
assert_eq!(json, "{\"tag_hex\":\"42424242424242424242424242424242\"}");
let deserialized: SeHdrTagV2 = serde_json::from_str(&json).expect("should deserialize");
assert_eq!(tag, deserialized);
}
#[test]
fn roundtrip_se_hdr_conf_v2_json() {
let conf = SeHdrConfV2 {
cck: Confidential::new([0x11; 32]),
xts: Confidential::new([0x22; SymKeyType::AES_256_XTS_KEY_LEN]),
psw: PSW {
addr: 0x1000,
mask: 0x2000,
},
scf: 0x42,
noi: 0,
res2: 0,
opt_items: vec![],
};
let json = serde_json::to_string(&conf).expect("should serialize");
assert_eq!(json, "{\"cck_hex\":\"1111111111111111111111111111111111111111111111111111111111111111\",\"xts_hex\":\"22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222\",\"psw\":{\"mask_hex\":\"0000000000002000\",\"addr_hex\":\"0000000000001000\"},\"scf_hex\":\"0000000000000042\"}");
let deserialized: SeHdrConfV2 = serde_json::from_str(&json).expect("should deserialize");
assert_eq!(conf, deserialized);
}
#[test]
fn roundtrip_se_hdr_aad_v2_json() {
let aad = SeHdrAadV2 {
sehs: 1024,
iv: [0x33; SymKeyType::AES_256_GCM_IV_LEN],
res1: 0,
nks: 2,
sea: 512,
nep: 10,
pcf: 0x100,
cust_pub_key: EcPubKeyCoordV1 { coord: [0x44; 160] },
pld: [0x55; SHA_512_HASH_LEN],
ald: [0x66; SHA_512_HASH_LEN],
tld: [0x77; SHA_512_HASH_LEN],
keyslots: vec![],
};
let json = serde_json::to_string(&aad).expect("should serialize");
assert_eq!(json, "{\"sehs\":1024,\"iv_hex\":\"333333333333333333333333\",\"nks\":2,\"sea\":512,\"nep\":10,\"pcf_hex\":\"0000000000000100\",\"cust_pub_key\":{\"coord_hex\":\"44444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444\"},\"pld_hex\":\"55555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555\",\"ald_hex\":\"66666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666\",\"tld_hex\":\"77777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777\",\"keyslots\":[]}");
let deserialized: SeHdrAadV2 = serde_json::from_str(&json).expect("should deserialize");
assert_eq!(aad, deserialized);
}
#[test]
fn roundtrip_se_hdr_bin_v2_json() {
let bin = SeHdrBinV2 {
aad: SeHdrAadV2 {
sehs: 1024,
iv: [0x33; SymKeyType::AES_256_GCM_IV_LEN],
res1: 0,
nks: 0,
sea: 64,
nep: 10,
pcf: 0x100,
cust_pub_key: EcPubKeyCoordV1 { coord: [0x44; 160] },
pld: [0x55; SHA_512_HASH_LEN],
ald: [0x66; SHA_512_HASH_LEN],
tld: [0x77; SHA_512_HASH_LEN],
keyslots: vec![],
},
data: vec![0x88; 64],
tag: SeHdrTagV2 {
tag: [0x99; SymKeyType::AES_256_GCM_TAG_LEN],
},
};
let json = serde_json::to_string(&bin).expect("should serialize");
assert_eq!(json, "{\"sehs\":1024,\"iv_hex\":\"333333333333333333333333\",\"nks\":0,\"sea\":64,\"nep\":10,\"pcf_hex\":\"0000000000000100\",\"cust_pub_key\":{\"coord_hex\":\"44444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444\"},\"pld_hex\":\"55555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555\",\"ald_hex\":\"66666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666\",\"tld_hex\":\"77777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777\",\"keyslots\":[],\"cipher_data_b64\":\"iIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiA==\",\"tag_hex\":\"99999999999999999999999999999999\"}");
let deserialized: SeHdrBinV2 = serde_json::from_str(&json).expect("should deserialize");
assert_eq!(bin, deserialized);
}
}

View File

@@ -10,7 +10,7 @@ use deku::ctx::Endian;
use deku::{DekuRead, DekuWrite};
use openssl::hash::{hash, MessageDigest};
use openssl::pkey::{PKey, PKeyRef, Public};
use pv::request::EcPubKeyCoord;
use pv::request::{EcPubKeyCoord, HybridPKey};
use pv::static_assert;
use serde::{Deserialize, Serialize};
use utils::HexSlice;
@@ -30,6 +30,20 @@ pub fn phkh_v1<T: AsRef<PKeyRef<Public>>>(key: T) -> Result<[u8; 32]> {
try_copy_slice_to_array(&binding)
}
/// Try to hash the public hybrid EC + ML-KEM key.
///
/// # Errors
///
/// This function will return an error if OpenSSL could not hash the key.
pub fn phkh_v2(key: &HybridPKey) -> Result<[u8; 64]> {
let mut buf: Vec<u8> = vec![];
let phk: EcPubKeyCoord = key.ec_key().try_into()?;
buf.extend_from_slice(phk.as_ref());
buf.extend_from_slice(&key.mlkem_key().raw_public_key()?);
let binding = hash(MessageDigest::sha512(), &buf)?;
try_copy_slice_to_array(&binding)
}
#[derive(Debug, Clone, PartialEq, Eq, DekuRead, DekuWrite, Serialize, Deserialize)]
#[deku(endian = "endian", ctx = "endian: Endian", ctx_default = "Endian::Big")]
pub struct EcPubKeyCoordV1 {
@@ -112,6 +126,58 @@ impl TryFrom<Vec<u8>> for BinaryKeySlotV1 {
}
}
#[repr(C)]
#[derive(Debug, PartialEq, Eq, Clone, DekuRead, DekuWrite, Serialize, Deserialize)]
#[deku(endian = "endian", ctx = "endian: Endian", ctx_default = "Endian::Big")]
/// Binary key slot v1
pub struct BinaryKeySlotV2 {
#[serde(with = "serde_hex_array", rename = "phkh_hex")]
/// Public host key hash
pub phkh: [u8; 64],
#[serde(with = "serde_hex_array", rename = "wrpk_hex")]
/// Wrapper key
pub wrpk: [u8; 32],
/// Tag
#[serde(with = "serde_hex_array", rename = "kst_hex")]
pub kst: [u8; 16],
#[serde(with = "serde_hex_array", rename = "kc_hex")]
/// Ciohertext
pub kc: [u8; 1568],
}
static_assert!(size_of::<BinaryKeySlotV2>() == 1680);
impl Display for BinaryKeySlotV2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "target key hash: {:}", HexSlice::from(&self.phkh))
}
}
impl Default for BinaryKeySlotV2 {
fn default() -> Self {
BinaryKeySlotV2 {
phkh: [0_u8; 64],
wrpk: [0_u8; 32],
kst: [0_u8; 16],
kc: [0_u8; 1568],
}
}
}
impl TryFrom<Vec<u8>> for BinaryKeySlotV2 {
type Error = Error;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
let data: [u8; 1680] = try_copy_slice_to_array(&value)?;
let bin = Self {
phkh: data[..64].try_into().unwrap(),
wrpk: data[64..96].try_into().unwrap(),
kst: data[96..112].try_into().unwrap(),
kc: data[112..].try_into().unwrap(),
};
Ok(bin)
}
}
#[cfg(test)]
mod serde_tests {
use super::*;

View File

@@ -7,7 +7,7 @@ use std::io::{BufRead, BufReader, Read};
use enum_dispatch::enum_dispatch;
use pv::misc::decode_hex;
use super::try_copy_slice_to_array;
use super::{try_copy_slice_to_array, KeyExchangeTrait, SeHdr};
use crate::error::{Error, Result};
/// The `enum_dispatch` macros needs at least one local trait to be implemented.
@@ -41,15 +41,116 @@ impl UvKeyHashV1 {
}
}
use std::fmt::{self, Display};
use std::ops::{Index, IndexMut};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UvKeyHashV1Kind {
PCHKH,
PBHKH,
PCHHKH,
PBHHKH,
}
impl Display for UvKeyHashV1Kind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::PCHKH => write!(f, "Classical Host key hash"),
Self::PBHKH => write!(f, "Backup classical host key hash"),
Self::PCHHKH => write!(f, "Hybrid host key hash"),
Self::PBHHKH => write!(f, "Backup hybrid host key hash"),
}
}
}
/// Index into the UV key hash array.
///
/// The indices follow the UV specification layout:
/// - 0: PCHKH (Classical host key hash)
/// - 1: PBHKH (Backup classical host key hash)
/// - 2-3: Reserved for future use
/// - 4: PCHHKH (Hybrid host key hash)
/// - 5: PBHHKH (Backup hybrid host key hash)
/// - 6-14: Reserved for future use
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UvKeyHashIdx(u8);
impl UvKeyHashIdx {
pub const PCHKH: Self = Self(0);
pub const PBHKH: Self = Self(1);
pub const PCHHKH: Self = Self(4);
pub const PBHHKH: Self = Self(5);
pub fn index(self) -> usize {
self.0 as usize
}
pub fn kind(self) -> Option<UvKeyHashV1Kind> {
match self.0 {
0 => Some(UvKeyHashV1Kind::PCHKH),
1 => Some(UvKeyHashV1Kind::PBHKH),
4 => Some(UvKeyHashV1Kind::PCHHKH),
5 => Some(UvKeyHashV1Kind::PBHHKH),
_ => None,
}
}
}
impl TryFrom<usize> for UvKeyHashIdx {
type Error = ();
fn try_from(value: usize) -> Result<Self, Self::Error> {
match value {
0..=14 => Ok(Self(value as u8)),
_ => Err(()),
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct UvKeyHashesV1 {
pub pchkh: UvKeyHashV1,
pub pbhkh: UvKeyHashV1,
pub res: [UvKeyHashV1; 13],
pub hashes: [UvKeyHashV1; 15],
}
impl Index<UvKeyHashIdx> for UvKeyHashesV1 {
type Output = UvKeyHashV1;
fn index(&self, pos: UvKeyHashIdx) -> &Self::Output {
&self.hashes[pos.index()]
}
}
impl IndexMut<UvKeyHashIdx> for UvKeyHashesV1 {
fn index_mut(&mut self, pos: UvKeyHashIdx) -> &mut Self::Output {
&mut self.hashes[pos.index()]
}
}
#[derive(Debug)]
pub struct MatchingUvKeyHash<'a> {
pub idx: UvKeyHashIdx,
pub hash: &'a UvKeyHashV1,
}
impl UvKeyHashesV1 {
pub fn matching_hashes(&self, hdr: &SeHdr) -> Vec<MatchingUvKeyHash<'_>> {
self.hashes
.iter()
.enumerate()
.filter(|(_, hash)| hdr.contains_hash(hash))
.filter_map(|(idx, hash)| {
Some(MatchingUvKeyHash {
idx: idx.try_into().ok()?,
hash,
})
})
.collect()
}
}
impl UvKeyHashV1 {
pub const UV_KEY_HASH_NULL: Self = Self([0x0_u8; 32]);
pub const UV_KEY_HASH_SIZE: usize = 32;
pub const UV_KEY_HASH_NULL: Self = Self([0x0_u8; Self::UV_KEY_HASH_SIZE]);
}
impl AsRef<[u8]> for UvKeyHashV1 {
@@ -107,9 +208,8 @@ impl UvKeyHashesV1 {
return Err(Error::InvalidUvKeyHashes);
}
let [pchkh, pbhkh, res @ ..]: [UvKeyHashV1; 15] =
hashes.try_into().map_err(|_| Error::InvalidUvKeyHashes)?;
Ok(Self { pchkh, pbhkh, res })
let hashes: [UvKeyHashV1; 15] = hashes.try_into().map_err(|_| Error::InvalidUvKeyHashes)?;
Ok(Self { hashes })
}
}
@@ -141,21 +241,18 @@ mod tests {
0000000000000000000000000000000000000000000000000000000000000000
";
let result = UvKeyHashesV1::read_from_io(Cursor::new(data)).expect("should not fail");
assert_eq!(
result,
UvKeyHashesV1 {
pchkh: UvKeyHashV1::new(
decode_hex("0b729fd62241b339840d61b964a06bb6a1fd4976d9ebea2b4fb48d44de3a2461")
.unwrap()
)
.unwrap(),
pbhkh: UvKeyHashV1::new(
decode_hex("8ec6bc2f77d5d6474b1417cf0a8c914f576245a5b9bb0eefacc7b821483ece7d")
.unwrap()
)
.unwrap(),
res: [UvKeyHashV1::UV_KEY_HASH_NULL; 13],
}
);
let mut exp_hashes = [UvKeyHashV1::UV_KEY_HASH_NULL; 15];
exp_hashes[0] = UvKeyHashV1::new(
decode_hex("0b729fd62241b339840d61b964a06bb6a1fd4976d9ebea2b4fb48d44de3a2461").unwrap(),
)
.unwrap();
exp_hashes[1] = UvKeyHashV1::new(
decode_hex("8ec6bc2f77d5d6474b1417cf0a8c914f576245a5b9bb0eefacc7b821483ece7d").unwrap(),
)
.unwrap();
let uv_hashes = UvKeyHashesV1 { hashes: exp_hashes };
assert_eq!(result, uv_hashes);
}
}

View File

@@ -3,12 +3,10 @@
// Copyright IBM Corp. 2024
use enum_dispatch::enum_dispatch;
use pv::request::openssl::pkey::{PKey, PKeyRef, Private, Public};
use pv::request::{
decrypt_aead, derive_aes256_gcm_key, encrypt_aead, Confidential, SymKey, SymKeyType,
};
use pv::request::openssl::pkey::{PKey, Public};
use pv::request::{decrypt_aead, encrypt_aead, Confidential, SymKey, SymKeyType};
use super::se_hdr::{SeHdrBinV1, SeHdrData, SeHdrVersioned};
use super::se_hdr::{SeHdrBinV1, SeHdrBinV2, SeHdrData, SeHdrVersioned};
use crate::pv_utils::error::{Error, Result};
use crate::pv_utils::serializing::deserialize_from_bytes;
@@ -54,15 +52,19 @@ pub trait AeadPlainDataTrait {
}
/// Key exchange related methods
#[enum_dispatch]
pub trait KeyExchangeTrait {
/// Checks if a public key was used.
type TargetKeyType;
type PrivateKeyType: ToOwned;
/// Checks if a public target key was used.
///
/// # Errors
///
/// This function will return an error if the public key cannot be converted
/// into a hash.
fn contains<K: AsRef<PKeyRef<Public>>>(&self, key: K) -> Result<bool>;
fn contains<K>(&self, key: K) -> Result<bool>
where
K: AsRef<Self::TargetKeyType>;
/// Checks if the hash of a public key was used.
fn contains_hash<H: AsRef<[u8]>>(&self, hash: H) -> bool;
@@ -79,22 +81,24 @@ pub trait KeyExchangeTrait {
/// Returns the key type of the exchanged key.
fn key_type(&self) -> SymKeyType;
/// Derive the key.
///
/// # Errors
///
/// This function will return an error if there is no customer public key is
/// available or the key derivations fails.
fn derive_key<K: AsRef<PKeyRef<Private>>>(&mut self, other_priv_key: K) -> Result<SymKey> {
match self.key_type() {
SymKeyType::Aes256Gcm => Ok(derive_aes256_gcm_key(
other_priv_key.as_ref(),
self.cust_pub_key()?.as_ref(),
)?
.into()),
_ => unreachable!("BUG"),
}
}
// TODO Implement it
// /// Derive the key.
// ///
// /// # Errors
// ///
// /// This function will return an error if there is no customer public key is
// /// available or the key derivations fails.
// fn derive_key<K>(&mut self, other_priv_key: K) -> Result<SymKey> where
// K: AsRef<Self::PrivateKeyType>{
// match self.key_type() {
// SymKeyType::Aes256Gcm =>
// match Self::TargetKeyType {
// PKeyRef::<Public> => {todo!()},
// HybridPKey => {todo!()},
// }
// _ => unreachable!("BUG"),
// }
// }
}
/// Trait to be used for plain UV data.

View File

@@ -2,13 +2,14 @@
//
// Copyright IBM Corp. 2024
use std::fmt::Display;
use enum_dispatch::enum_dispatch;
use openssl::pkey::{PKey, PKeyRef, Private, Public};
use openssl::pkey::{PKey, Private};
use pv::request::{Confidential, SymKey};
use super::Error;
use crate::pv_utils::error::Result;
use crate::pv_utils::se_hdr::SeHdrData;
use crate::pv_utils::uvdata::{AeadCipherTrait, UvDataPlainTrait};
#[enum_dispatch]
@@ -20,33 +21,33 @@ pub trait AeadCipherBuilderTrait: AeadCipherTrait {
}
/// Key exchange related methods
#[enum_dispatch]
pub trait KeyExchangeBuilderTrait {
type TargetKeyType: ToOwned;
type AeadKeyType: Display + std::fmt::Debug;
type PrivateKeyType: ToOwned;
fn add_keyslot(
&mut self,
hostkey: &PKeyRef<Public>,
aead_key: &SymKey,
priv_key: &PKeyRef<Private>,
hostkey: &Self::TargetKeyType,
aead_key: &Self::AeadKeyType,
priv_key: &Self::PrivateKeyType,
) -> Result<()>;
fn clear_keyslots(&mut self) -> Result<()>;
// TODO How to handle PKey vs &PKeyRef?
fn generate_private_key(&self) -> Result<PKey<Private>>;
fn set_cust_public_key(&mut self, key: &PKeyRef<Private>) -> Result<()>;
fn set_cust_public_key(&mut self, key: &Self::PrivateKeyType) -> Result<()>;
}
pub struct UvDataBuilder<
'a,
T: KeyExchangeBuilderTrait + AeadCipherBuilderTrait,
K = PKeyRef<Public>,
P = PKey<Private>,
> {
pub struct UvDataBuilder<T: KeyExchangeBuilderTrait + AeadCipherBuilderTrait> {
pub(crate) expert_mode: bool,
pub(crate) prot_key: SymKey,
pub(crate) priv_key: P,
pub(crate) target_keys: Vec<&'a K>,
pub(crate) prot_key: T::AeadKeyType,
pub(crate) priv_key: T::PrivateKeyType,
pub(crate) target_keys: Vec<T::TargetKeyType>,
pub(crate) plain_data: T,
}
impl<T: KeyExchangeBuilderTrait + AeadCipherBuilderTrait, K, P> UvDataBuilder<'_, T, K, P> {
impl<T: KeyExchangeBuilderTrait + AeadCipherBuilderTrait> UvDataBuilder<T> {
/// Enable expert mode - this is required for specifying PSW, etc.
pub fn i_know_what_i_am_doing(&mut self) {
self.expert_mode = true;
@@ -54,7 +55,7 @@ impl<T: KeyExchangeBuilderTrait + AeadCipherBuilderTrait, K, P> UvDataBuilder<'_
}
impl<T: std::fmt::Debug + KeyExchangeBuilderTrait + AeadCipherBuilderTrait + UvDataPlainTrait>
std::fmt::Debug for UvDataBuilder<'_, T>
std::fmt::Debug for UvDataBuilder<T>
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UvDataBuilder")
@@ -65,15 +66,18 @@ impl<T: std::fmt::Debug + KeyExchangeBuilderTrait + AeadCipherBuilderTrait + UvD
}
}
impl<'a, T: KeyExchangeBuilderTrait + AeadCipherBuilderTrait> UvDataBuilder<'a, T> {
pub fn add_hostkeys<P: AsRef<PKeyRef<Public>>>(
&mut self,
hostkeys: &'a [P],
) -> Result<&mut Self> {
impl<T> UvDataBuilder<T>
where
T: KeyExchangeBuilderTrait<AeadKeyType = SymKey> + AeadCipherBuilderTrait,
{
pub fn add_hostkeys(&mut self, hostkeys: &[T::TargetKeyType]) -> Result<&mut Self>
where
T::TargetKeyType: Clone,
{
for hk in hostkeys {
self.plain_data
.add_keyslot(hk.as_ref(), &self.prot_key, &self.priv_key)?;
self.target_keys.push(hk.as_ref());
.add_keyslot(hk, &self.prot_key, &self.priv_key)?;
self.target_keys.push(hk.clone());
}
Ok(self)
@@ -89,9 +93,9 @@ impl<'a, T: KeyExchangeBuilderTrait + AeadCipherBuilderTrait> UvDataBuilder<'a,
fn update_target_key_slots(&mut self) -> Result<()> {
self.plain_data.clear_keyslots()?;
for hk in &self.target_keys {
for hostkey in &self.target_keys {
self.plain_data
.add_keyslot(hk, &self.prot_key, &self.priv_key)?;
.add_keyslot(hostkey, &self.prot_key, &self.priv_key)?;
}
Ok(())
}
@@ -100,6 +104,7 @@ impl<'a, T: KeyExchangeBuilderTrait + AeadCipherBuilderTrait> UvDataBuilder<'a,
if !self.expert_mode {
return Err(Error::NonExpertMode);
}
// TODO Implement TryFrom<...> ?!
let key = SymKey::try_from_data(self.plain_data.aead_key_type(), data)?;
self.prot_key = key;
self.update_target_key_slots()?;
@@ -107,23 +112,26 @@ impl<'a, T: KeyExchangeBuilderTrait + AeadCipherBuilderTrait> UvDataBuilder<'a,
Ok(self)
}
pub fn with_priv_key(&mut self, priv_key: &PKeyRef<Private>) -> Result<&mut Self> {
pub fn with_priv_key(&mut self, priv_key: &T::PrivateKeyType) -> Result<&mut Self>
where
T::PrivateKeyType: Clone,
{
if !self.expert_mode {
return Err(Error::NonExpertMode);
}
self.plain_data.set_cust_public_key(priv_key)?;
self.priv_key = priv_key.to_owned();
self.priv_key = priv_key.clone();
self.update_target_key_slots()?;
Ok(self)
}
pub const fn prot_key(&self) -> &SymKey {
pub const fn prot_key(&self) -> &<T as KeyExchangeBuilderTrait>::AeadKeyType {
&self.prot_key
}
pub fn priv_key(&self) -> &PKeyRef<Private> {
self.priv_key.as_ref()
pub fn priv_key(&self) -> &<T as KeyExchangeBuilderTrait>::PrivateKeyType {
&self.priv_key
}
}

View File

@@ -10,17 +10,14 @@ use std::rc::Rc;
use anyhow::{anyhow, Context, Result};
use deku::DekuContainerRead;
use log::debug;
use openssl::pkey::{PKey, Public};
use pv::misc::read_file;
use pv::request::Confidential;
use pv::request::{Confidential, HostKey};
use pvimg::error::Error;
use pvimg::misc::{round_up, serialize_to_bytes, ShortPsw, PSW, PSW_MASK_BA, PSW_MASK_EA};
use pvimg::secured_comp::{
ComponentTrait, Interval, Layout, SecuredComponent, SecuredComponentBuilder,
};
use pvimg::uvdata::{
BuilderTrait, PlaintextControlFlagsV1, SeHdrBuilder, SeHdrVersion, SecretControlFlagsV1,
};
use pvimg::uvdata::{BuilderTrait, EffectiveControlFlags, SeHdrBuilder, SeHdrFlag, SeHdrVersion};
use crate::se_img_comps::ipib::Ipib;
use crate::se_img_comps::kernel::S390Kernel;
@@ -33,9 +30,9 @@ use crate::se_img_comps::{
};
pub struct SeHdrArgs<'a> {
pub keys: &'a [PKey<Public>],
pub pcf: &'a PlaintextControlFlagsV1,
pub scf: &'a SecretControlFlagsV1,
pub keys: &'a [HostKey],
pub pcf: &'a EffectiveControlFlags<SeHdrFlag>,
pub scf: &'a EffectiveControlFlags<SeHdrFlag>,
pub cck: &'a Option<(PathBuf, Confidential<Vec<u8>>)>,
pub hdr_aead_key: &'a Option<(PathBuf, Confidential<Vec<u8>>)>,
pub psw_addr: &'a Option<u64>,
@@ -95,7 +92,7 @@ impl<W: Write + Seek> SeImgBuilder<W> {
/// Create a Secure Execution boot image builder
#[allow(clippy::similar_names)]
pub(crate) fn new_v1(
pub(crate) fn new(
mut writer: W,
encryption: bool,
legacy_expected_se_hdr_size: Option<usize>,
@@ -322,8 +319,25 @@ impl<W: Write + Seek> SeImgBuilder<W> {
fn add_sehdr(&mut self, stage3b_entry: u64, sehdr_args: SeHdrArgs) -> Result<Rc<ImgComponent>> {
let meta = self.builder.finish()?;
// Determine version from first key (all keys should be same version)
let version = match sehdr_args.keys.first() {
Some(HostKey::V1(_)) => SeHdrVersion::V1,
Some(HostKey::V2(_)) => SeHdrVersion::V2,
Some(_) => unreachable!("Unknown HostKey version"),
None => return Err(Error::NoHostkey.into()),
};
// Verify all hostkeys have the same version -> test if all or none are hybrid
let hybrid = matches!(version, SeHdrVersion::V2);
sehdr_args
.keys
.iter()
.all(|k| k.is_hybrid() == hybrid)
.then_some(())
.ok_or(pv::Error::MixedHostkeyVersions)?;
let mut se_hdr_builder = SeHdrBuilder::new(
SeHdrVersion::V1,
version,
PSW {
addr: sehdr_args.psw_addr.unwrap_or(stage3b_entry),
mask: Self::DEFAULT_INITIAL_PSW_MASK,
@@ -497,7 +511,7 @@ mod tests {
let encryption = true;
let mut writer = Cursor::new(Vec::new());
let ctx_res = SeImgBuilder::new_v1(&mut writer, encryption, None, None);
let ctx_res = SeImgBuilder::new(&mut writer, encryption, None, None);
assert!(ctx_res.is_ok());
let ctx = ctx_res.unwrap();

View File

@@ -7,7 +7,8 @@ use std::fmt::Display;
use clap::error::ErrorKind::ValueValidation;
use clap::{ArgGroup, Args, CommandFactory, Parser, Subcommand, ValueEnum, ValueHint};
use utils::{
combined_path_opt, combined_path_req, CertificateOptions, DeprecatedVerbosityOptions, STDOUT,
combined_path_opt, combined_path_req, AutoOrExplicit, CertificateOptions,
DeprecatedVerbosityOptions, HkdVersion, ValueEnumDisplay, ValueEnumFromStr, STDOUT,
};
/// Manage secrets for IBM Secure Execution guests.
@@ -34,6 +35,28 @@ pub enum CreateSecretFlags {
DisableDump,
}
/// Secure Execution add secret version for CLI
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, ValueEnumDisplay, ValueEnumFromStr)]
pub enum SecretVersion {
#[value(name = "1")]
/// Version 1 - uses traditional cryptographic keys
V1,
#[value(name = "2")]
/// Version 2 - uses hybrid (post-quantum) cryptographic keys
V2,
}
pub type SecretVersionSelection = AutoOrExplicit<SecretVersion>;
impl From<SecretVersion> for HkdVersion {
fn from(val: SecretVersion) -> Self {
match val {
SecretVersion::V1 => Self::Classical,
SecretVersion::V2 => Self::Hybrid,
}
}
}
#[derive(Args, Debug)]
#[command(group(ArgGroup::new("as-ext").args(["cck", "extension_secret"])),)]
pub struct CreateSecretOpt {
@@ -173,6 +196,10 @@ pub struct CreateSecretOpt {
/// Ignored for meta-secrets.
#[arg(long)]
pub use_name: bool,
/// Specify the Add-secret version to use.
#[arg(long = "secret-version", value_name = "VERSION", default_value_t = SecretVersion::V1)]
pub secret_version: SecretVersion,
}
#[derive(Subcommand, Debug)]

View File

@@ -8,7 +8,7 @@ pub use create::create;
mod verify;
pub use verify::verify;
pub const CMD_FN: &[&str] = &["+create", "+verify"];
pub const CMD_FN: &[&str] = &["+create", "+verify", "+quantumsafe"];
#[cfg(target_arch = "s390x")]
mod add;

View File

@@ -22,7 +22,10 @@ use serde_yaml::Value;
use utils::get_writer_from_cli_file_arg;
use zerocopy::IntoBytes;
use crate::cli::{AddSecretType, CreateSecretFlags, CreateSecretOpt, RetrieveableSecretInpKind};
use crate::cli::{
AddSecretType, CreateSecretFlags, CreateSecretOpt, RetrieveableSecretInpKind, SecretVersion,
SecretVersionSelection,
};
fn write_out<P, D>(path: &P, data: D, ctx: &str) -> pv::Result<()>
where
@@ -83,6 +86,41 @@ fn retrievable(name: &str, secret: &str, kind: &RetrieveableSecretInpKind) -> Re
.map_err(Error::from)
}
/// Auto-detect the Add-secret version based on the host keys.
///
/// Returns Two if any host key is a hybrid key, otherwise returns V1.
fn auto_detect_version(host_keys: &[HostKey]) -> AddSecretVersion {
let use_hybrid_keys = host_keys.iter().any(|k: &HostKey| k.is_hybrid());
if use_hybrid_keys {
AddSecretVersion::Two
} else {
AddSecretVersion::One
}
}
impl From<SecretVersion> for AddSecretVersion {
fn from(value: SecretVersion) -> Self {
match value {
SecretVersion::V1 => Self::One,
SecretVersion::V2 => Self::Two,
}
}
}
/// Determine the attestation version to use.
///
/// If an explicit version is provided via CLI, use that.
/// Otherwise, auto-detect based on the host key types.
fn determine_version(
cli_version: SecretVersionSelection,
host_keys: &[HostKey],
) -> AddSecretVersion {
match cli_version {
SecretVersionSelection::Auto => auto_detect_version(host_keys),
SecretVersionSelection::Explicit(att_version) => att_version.into(),
}
}
/// Prepare an add-secret request
pub fn create(opt: &CreateSecretOpt) -> Result<()> {
if pv_guest_bit_set() {
@@ -94,17 +132,9 @@ pub fn create(opt: &CreateSecretOpt) -> Result<()> {
}
}
let mut asrcb = build_asrcb(opt)?;
let asrcb = build_asrcb(opt)?;
debug!("Generated Add-secret request");
// Add host-key documents
opt.certificate_args
.get_verified_hkds("secret")?
.into_iter()
.for_each(|k| asrcb.add_hostkey(HostKey::V1(k)));
debug!("Added all host-keys");
// build + encrypt the request
let rq =
ReqEncrCtx::random(SymKeyType::Aes256Gcm).context("Failed to generate random input")?;
@@ -168,9 +198,23 @@ fn build_asrcb(opt: &CreateSecretOpt) -> Result<AddSecretRequest> {
debug!("FLAGS: {flags:x?}");
let mut se_hdr = open_file(&opt.hdr)?;
let (tags, _) = BootHdrTags::from_se_image(&mut se_hdr)
let (boot_tags, _) = BootHdrTags::from_se_image(&mut se_hdr)
.with_context(|| format!("Provided SE-header in '{}' is malformed", &opt.hdr))?;
let mut asrcb = AddSecretRequest::new(AddSecretVersion::One, secret, tags, flags);
let hkds = opt.certificate_args.get_verified_hkds(
"secret",
SecretVersionSelection::Explicit(opt.secret_version).map(|v| v.into()),
)?;
let secret_version =
determine_version(SecretVersionSelection::Explicit(opt.secret_version), &hkds);
let mut asrcb = AddSecretRequest::new(secret_version, secret, boot_tags, flags)?;
for k in hkds.into_iter() {
asrcb.add_hostkey(k)?
}
debug!("Added all host-keys");
// Set CUID
read_cuid(&mut asrcb, opt)?;

View File

@@ -6,7 +6,7 @@
use std::env;
use std::io::Error;
use clap::{CommandFactory, ValueEnum};
use clap::CommandFactory;
use clap_complete::{generate_to, Shell};
include!("src/cli.rs");

View File

@@ -3,11 +3,36 @@
// Copyright IBM Corp. 2025
use std::sync::OnceLock;
use clap::{ArgAction, Parser};
use utils::CertificateOptions;
use clap::{ArgAction, Parser, ValueEnum};
use utils::{
AutoOrExplicit, AutoOrExplicitParser, CertificateOptions, HkdVersion, ValueEnumDisplay,
ValueEnumFromStr,
};
static VERSION: OnceLock<String> = OnceLock::new();
/// Secure Execution HostKey version for CLI
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, ValueEnumDisplay, ValueEnumFromStr)]
pub enum HostKeyVersion {
#[value(name = "1")]
/// Version 1 - uses traditional cryptographic keys
V1,
#[value(name = "2")]
/// Version 2 - uses hybrid (post-quantum) cryptographic keys
V2,
}
pub type HostKeyVersionSelection = AutoOrExplicit<HostKeyVersion>;
pub type HostKeyVersionSelectionParser = AutoOrExplicitParser<HostKeyVersion>;
impl From<HostKeyVersion> for HkdVersion {
fn from(val: HostKeyVersion) -> Self {
match val {
HostKeyVersion::V1 => Self::Classical,
HostKeyVersion::V2 => Self::Hybrid,
}
}
}
#[derive(Parser, Debug)]
#[command(long_version=ver(), disable_version_flag(true))]
/// Tool to verify host-keys
@@ -23,6 +48,10 @@ pub struct CliOptions {
#[arg(long, action=ArgAction::Version)]
/// Print version information and exit.
version: (),
/// Specify the Host-key version to use.
#[arg(long = "hkd-version", value_name = "VERSION", default_value_t = HostKeyVersionSelection::Auto, value_parser = HostKeyVersionSelectionParser::default())]
pub hkd_version: HostKeyVersionSelection,
}
fn ver() -> &'static str {

View File

@@ -14,9 +14,9 @@ static LOGGER: PvLogger = PvLogger;
fn main() -> Result<()> {
LOGGER.start(LevelFilter::Trace)?;
cli::CliOptions::parse()
.certificate_args
.get_verified_hkds("info")?;
let opt = cli::CliOptions::parse();
opt.certificate_args
.get_verified_hkds("info", opt.hkd_version.map(|v| v.into()))?;
info!("Host-key documents verified.");
Ok(())
}

View File

@@ -9,8 +9,11 @@ chrono = { version = "0.4.44", default-features = false, features = ["std"] }
clap = { version ="4.6", features = ["derive", "wrap_help"] }
libc = "0.2.186"
log = { version = "0.4.29", features = ["std", "release_max_level_debug"] }
openssl = "0.10"
pv = { path = "../pv", package = "s390_pv" }
serde = { version = "1.0.228"}
utils_macros = { path = "../utils_macros" }
[dev-dependencies]
serde_json = "1.0.149"

View File

@@ -1,17 +1,136 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2023, 2024
// Copyright IBM Corp.
use std::fmt::Display;
use std::io::{Read, Write};
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::str::FromStr;
use clap::{ArgAction, ArgGroup, Args, Command, ValueHint};
use log::{info, warn, LevelFilter};
use pv::misc::{create_file, open_file, read_certs, read_file};
use pv::request::openssl::pkey::{PKey, Public};
use pv::request::HkdVerifier;
use pv::{Error, Result};
use clap::builder::{EnumValueParser, PossibleValue, TypedValueParser};
use clap::{Arg, ArgAction, ArgGroup, Args, Command, ValueEnum, ValueHint};
use log::LevelFilter;
use pv::misc::{create_file, open_file};
use pv::request::{HkdVerifier, HostKey};
use pv::Result;
use crate::hkd::{HkdLoader, HkdVersionSelection};
/// Generic version selection for CLI
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AutoOrExplicit<T> {
Auto,
Explicit(T),
}
impl<T> Display for AutoOrExplicit<T>
where
T: Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AutoOrExplicit::Auto => write!(f, "auto"),
AutoOrExplicit::Explicit(version) => write!(f, "{version}"),
}
}
}
impl<T> AutoOrExplicit<T> {
pub fn map<U, F>(self, f: F) -> AutoOrExplicit<U>
where
F: FnOnce(T) -> U,
{
match self {
AutoOrExplicit::Explicit(v) => AutoOrExplicit::Explicit(f(v)),
AutoOrExplicit::Auto => AutoOrExplicit::Auto,
}
}
}
impl<T> FromStr for AutoOrExplicit<T>
where
T: FromStr<Err = String>,
{
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"auto" => Ok(Self::Auto),
_ => {
let v = T::from_str(s)?;
Ok(Self::Explicit(v))
}
}
}
}
#[derive(Clone)]
pub struct AutoOrExplicitParser<T> {
_marker: PhantomData<T>,
}
impl<T> Default for AutoOrExplicitParser<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> AutoOrExplicitParser<T> {
pub fn new() -> Self {
Self {
_marker: PhantomData,
}
}
}
impl<T> TypedValueParser for AutoOrExplicitParser<T>
where
T: ValueEnum + FromStr + Clone + Send + Sync + Display + 'static,
T::Err: std::fmt::Display,
{
type Value = AutoOrExplicit<T>;
fn parse_ref(
&self,
cmd: &Command,
arg: Option<&Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::error::Error> {
let s = value
.to_str()
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8).with_cmd(cmd))?;
if s == format!("{}", Self::Value::Auto) {
Ok(Self::Value::Auto)
} else {
let parsed = s.parse::<T>().map_err(|_e| {
let mut err =
clap::error::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
clap::error::ContextKind::InvalidArg,
clap::error::ContextValue::String(arg.to_string()),
);
}
err.insert(
clap::error::ContextKind::InvalidValue,
clap::error::ContextValue::String(s.to_string()),
);
err
})?;
Ok(Self::Value::Explicit(parsed))
}
}
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
let enum_parser = EnumValueParser::<T>::new();
let mut values = vec![PossibleValue::new("auto")];
values.extend(enum_parser.possible_values()?);
Some(Box::new(values.into_iter()))
}
}
/// CLI Argument collection for handling host-keys, IBM signing keys, and certificates.
#[derive(Args, Debug, Clone, PartialEq, Eq, Default)]
@@ -108,40 +227,27 @@ impl CertificateOptions {
}
}
/// Read the host-keys specified and verifies them if required
/// Read the hybrid host-keys specified and verifies them if required
///
/// - `protectee`: what you want to create. e.g. add-secret request or SE-image
/// - `version`: requested host-key document version
///
/// # Error
/// Returns an error if something went wrong during parsing the HKDs, the verification chain
/// could not built, or when the verification
/// failed.
pub fn get_verified_hkds(&self, protectee: &'static str) -> Result<Vec<PKey<Public>>> {
pub fn get_verified_hkds(
&self,
protectee: &'static str,
requested_version: HkdVersionSelection,
) -> Result<Vec<HostKey>> {
let hkds = &self.host_key_documents;
let verifier = self.verifier(protectee)?;
let mut res = Vec::with_capacity(hkds.len());
for hkd in hkds {
let hk = read_file(hkd, "host-key document")?;
let certs = read_certs(&hk).map_err(|source| Error::HkdNotPemOrDer {
hkd: hkd.display().to_string(),
source,
})?;
if certs.is_empty() {
return Err(Error::NoHkdInFile(hkd.display().to_string()));
}
if certs.len() != 1 {
warn!(
"The host-key document in '{}' contains more than one certificate!",
hkd.display()
)
}
// Panic: len is == 1 -> unwrap will succeed/not panic
let c = certs.first().unwrap();
verifier.verify(c)?;
res.push(c.public_key()?);
info!("Use host-key document at '{}'", hkd.display());
let host_key = HkdLoader::load_and_verify(hkd, verifier.as_ref(), requested_version)?;
res.push(host_key);
}
Ok(res)
}

162
rust/utils/src/hkd.rs Normal file
View File

@@ -0,0 +1,162 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp.
use std::fmt::{Display, Formatter};
use std::path::Path;
use log::{error, info};
use openssl::nid::Nid;
use openssl::pkey::{Id, KeyType, PKeyRef, Public};
use openssl::x509::X509;
use pv::misc::{read_certs, read_file};
use pv::request::{HkdVerifier, HostKey, HybridPKey};
use pv::{Error, Result};
use crate::AutoOrExplicit;
/// Host key document version
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HkdVersion {
/// Version 1 - uses traditional cryptographic keys (1 certificate)
Classical,
/// Version 2 - uses hybrid (post-quantum) cryptographic keys (2 certificates)
Hybrid,
}
impl HkdVersion {
/// Get the required certificate count for this version
pub fn cert_count(self) -> usize {
match self {
HkdVersion::Classical => 1,
HkdVersion::Hybrid => 2,
}
}
}
impl Display for HkdVersion {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self {
HkdVersion::Classical => write!(f, "classical"),
HkdVersion::Hybrid => write!(f, "hybrid"),
}
}
}
pub type HkdVersionSelection = AutoOrExplicit<HkdVersion>;
/// Helper struct for loading and verifying host-key documents
pub struct HkdLoader;
impl HkdLoader {
fn detect_version(path: &Path, certs: &Vec<X509>) -> Result<HkdVersion> {
info!("Auto-detecting version of the host-key document format");
Ok(match certs.len() {
1 => HkdVersion::Classical,
2 => HkdVersion::Hybrid,
_ => {
error!(
"Invalid host-key document '{}': it contains more than two certificates, which is not supported by any host-key document format.",
path.display()
);
return Err(Error::WrongNumberOfKeys(path.display().to_string()));
}
})
}
fn validate_version(path: &Path, certs: &Vec<X509>, version: HkdVersion) -> Result<HkdVersion> {
if certs.len() != version.cert_count() {
error!(
"Host-key document '{}' is not a {} host-key document.",
path.display(),
version,
);
return Err(Error::WrongNumberOfKeys(path.display().to_string()));
}
Ok(version)
}
fn is_ec_p521_key(key: &PKeyRef<Public>) -> bool {
if key.id() == Id::EC {
if let Ok(ec_key) = key.ec_key() {
let group = ec_key.group();
if let Some(curve_nid) = group.curve_name() {
return curve_nid == Nid::SECP521R1;
}
}
}
false
}
fn is_mlkem1024_key(key: &PKeyRef<Public>) -> bool {
key.is_a(KeyType::ML_KEM_1024)
}
/// Load and verify a host-key document from a file
///
/// # Errors
///
/// This function will return an error if:
/// - The file cannot be read
/// - The content is not valid PEM or DER format
/// - The file contains no certificates or wrong number of certificates
/// - The public key cannot be extracted from the certificate(s)
/// - The verification fails
/// - Key types are invalid
pub fn load_and_verify<P: AsRef<Path>>(
path: P,
verifier: &dyn HkdVerifier,
requested_version: HkdVersionSelection,
) -> Result<HostKey> {
let path = path.as_ref();
let hk = read_file(path, "host-key document")?;
let certs = read_certs(&hk).map_err(|source| Error::HkdNotPemOrDer {
hkd: path.display().to_string(),
source,
})?;
if certs.is_empty() {
return Err(Error::NoHkdInFile(path.display().to_string()));
}
let version = match requested_version {
HkdVersionSelection::Auto => Self::detect_version(path, &certs)?,
HkdVersionSelection::Explicit(version) => {
Self::validate_version(path, &certs, version)?
}
};
info!("Using {version} host-key document format");
// SAFETY: certs is guaranteed to be non-empty due to the check
let c1 = certs
.first()
.expect("Certificate list validated as non-empty");
if !Self::is_ec_p521_key(c1.public_key()?.as_ref()) {
return Err(Error::InvalidHkd(
"First key must be a EC-p521 key".to_string(),
));
}
verifier.verify(c1)?;
match version {
HkdVersion::Classical => Ok(HostKey::V1(c1.public_key()?)),
HkdVersion::Hybrid => {
let c2 = &certs
.get(1)
.expect("Certificate list length was already checked");
if !Self::is_mlkem1024_key(c2.public_key()?.as_ref()) {
return Err(Error::InvalidHkd(
"Second key must be a ML-KEM 1024 key".to_string(),
));
}
verifier.verify(c2)?;
Ok(HostKey::V2(HybridPKey::new(
c1.public_key()?,
c2.public_key()?,
)?))
}
}
}
}

View File

@@ -7,21 +7,26 @@ mod cli;
mod exit_code;
mod file;
mod hexslice;
pub mod hkd;
mod hostname;
mod json;
mod log;
mod tmpfile;
pub use ::log::LevelFilter;
// Re-export procedural macros from utils_macros
pub use utils_macros::{ControlFlag, ValueEnumDisplay, ValueEnumFromStr};
pub use crate::cli::{
combined_path_opt, combined_path_req, get_reader_from_cli_file_arg,
get_writer_from_cli_file_arg, print_cli_error, print_error, CertificateOptions,
DeprecatedVerbosityOptions, VerbosityOptions, STDIN, STDOUT,
get_writer_from_cli_file_arg, print_cli_error, print_error, AutoOrExplicit,
AutoOrExplicitParser, CertificateOptions, DeprecatedVerbosityOptions, VerbosityOptions, STDIN,
STDOUT,
};
pub use crate::exit_code::{docstring, ExitCodeDoc, ExitCodeTrait, ExitCodeVariantDoc};
pub use crate::file::{AtomicFile, AtomicFileOperation};
pub use crate::hexslice::HexSlice;
pub use crate::hkd::{HkdLoader, HkdVersion, HkdVersionSelection};
pub use crate::hostname::gethostname;
pub use crate::json::S390ToolsMetaData;
pub use crate::log::PvLogger;

View File

@@ -0,0 +1,259 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp.
//! Integration tests for malformed host key document handling
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use pv::request::NoVerifyHkd;
use utils::{AutoOrExplicit, HkdLoader, HkdVersion, TemporaryDirectory};
/// Path to test certificate assets
fn cert_asset_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("pv/tests/assets/cert")
.join(name)
}
#[test]
fn test_empty_hkd_file() {
let temp_dir = TemporaryDirectory::new().unwrap();
let file_path = temp_dir.path().join("empty.hkd");
File::create(&file_path).unwrap();
let result = HkdLoader::load_and_verify(&file_path, &NoVerifyHkd, AutoOrExplicit::Auto);
assert!(result.is_err(), "Empty HKD file should be rejected");
let err = result.unwrap_err();
assert!(
matches!(err, pv::Error::NoHkdInFile(_)),
"Expected NoHkdInFile error, got: {:?}",
err
);
}
#[test]
fn test_invalid_pem_format() {
// Create a file with invalid PEM content
let temp_dir = TemporaryDirectory::new().unwrap();
let file_path = temp_dir.path().join("invalid.hkd");
let mut temp_file = File::create(&file_path).unwrap();
writeln!(temp_file, "-----BEGIN CERTIFICATE-----").unwrap();
writeln!(temp_file, "INVALID_BASE64_CONTENT!!!").unwrap();
writeln!(temp_file, "-----END CERTIFICATE-----").unwrap();
temp_file.flush().unwrap();
// Test that invalid PEM is properly rejected with HkdNotPemOrDer error
let result = HkdLoader::load_and_verify(&file_path, &NoVerifyHkd, AutoOrExplicit::Auto);
assert!(result.is_err(), "Invalid PEM format should be rejected");
let err = result.unwrap_err();
assert!(
matches!(err, pv::Error::HkdNotPemOrDer { .. }),
"Expected HkdNotPemOrDer error, got: {:?}",
err
);
}
#[test]
fn test_wrong_number_of_certificates_v1() {
// Create a file with three valid certificates (invalid count for v1, expects 1)
let temp_dir = TemporaryDirectory::new().unwrap();
let file_path = temp_dir.path().join("wrong_count_v1.hkd");
let mut temp_file = File::create(&file_path).unwrap();
let cert1 = fs::read_to_string(cert_asset_path("host.crt")).unwrap();
let cert2 = fs::read_to_string(cert_asset_path("ibm.crt")).unwrap();
let cert3 = fs::read_to_string(cert_asset_path("root_ca.crt")).unwrap();
write!(temp_file, "{}{}{}", cert1, cert2, cert3).unwrap();
temp_file.flush().unwrap();
let result = HkdLoader::load_and_verify(
&file_path,
&NoVerifyHkd,
AutoOrExplicit::Explicit(HkdVersion::Classical),
);
assert!(
result.is_err(),
"Wrong number of certificates for v1 should be rejected"
);
let err = result.unwrap_err();
assert!(
matches!(err, pv::Error::WrongNumberOfKeys(_)),
"Expected WrongNumberOfKeys error, got: {:?}",
err
);
}
#[test]
fn test_wrong_number_of_certificates_v2() {
// Use existing single certificate file directly (invalid count for v2, expects 2)
let file_path = cert_asset_path("host.crt");
// Test that wrong number of certificates is properly rejected with WrongNumberOfKeys error
let result = HkdLoader::load_and_verify(
&file_path,
&NoVerifyHkd,
AutoOrExplicit::Explicit(HkdVersion::Hybrid),
);
assert!(
result.is_err(),
"Wrong number of certificates for v2 should be rejected"
);
let err = result.unwrap_err();
assert!(
matches!(err, pv::Error::WrongNumberOfKeys(_)),
"Expected WrongNumberOfKeys error, got: {:?}",
err
);
}
#[test]
fn test_corrupted_certificate() {
// Create a file with corrupted certificate data
let temp_dir = TemporaryDirectory::new().unwrap();
let file_path = temp_dir.path().join("corrupted.hkd");
let mut temp_file = File::create(&file_path).unwrap();
writeln!(temp_file, "-----BEGIN CERTIFICATE-----").unwrap();
writeln!(
temp_file,
"MIICdTCCAd6gAwIBAgIBADANBgkqhkiG9w0BAQsFADBQMQswCQYDVQQGEwJVUzEL"
)
.unwrap();
writeln!(temp_file, "CORRUPTED_DATA_HERE").unwrap();
writeln!(temp_file, "-----END CERTIFICATE-----").unwrap();
temp_file.flush().unwrap();
let result = HkdLoader::load_and_verify(&file_path, &NoVerifyHkd, AutoOrExplicit::Auto);
assert!(result.is_err(), "Corrupted certificate should be rejected");
let err = result.unwrap_err();
assert!(
matches!(err, pv::Error::HkdNotPemOrDer { .. }),
"Expected HkdNotPemOrDer error, got: {:?}",
err
);
}
#[test]
fn test_wrong_key_type_v1() {
// Use RSA certificate instead of EC-p521 for v1 (should fail)
let file_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("pv/tests/assets/keys/rsa2048.crt");
let result = HkdLoader::load_and_verify(
&file_path,
&NoVerifyHkd,
AutoOrExplicit::Explicit(HkdVersion::Classical),
);
assert!(
result.is_err(),
"RSA key should be rejected for v1 (expects EC-p521)"
);
let err = result.unwrap_err();
if let pv::Error::InvalidHkd(msg) = err {
assert_eq!(
msg, "First key must be a EC-p521 key",
"Error message should indicate EC-p521 requirement"
);
} else {
panic!(
"Expected InvalidHkd error for wrong key type, got: {:?}",
err
);
}
}
#[test]
fn test_wrong_key_type_v2() {
// Create a file with two RSA certificates instead of EC-p521 + ML-KEM for v2
let temp_dir = TemporaryDirectory::new().unwrap();
let file_path = temp_dir.path().join("wrong_key_v2.hkd");
let mut temp_file = File::create(&file_path).unwrap();
// Read two RSA certificates and concatenate them
let rsa_cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("pv/tests/assets/keys/rsa2048.crt");
let rsa_cert = fs::read_to_string(&rsa_cert_path).unwrap();
write!(temp_file, "{}{}", rsa_cert, rsa_cert).unwrap();
temp_file.flush().unwrap();
// Test that wrong key types are properly rejected with InvalidHkd error
let result = HkdLoader::load_and_verify(
&file_path,
&NoVerifyHkd,
AutoOrExplicit::Explicit(HkdVersion::Hybrid),
);
assert!(
result.is_err(),
"RSA keys should be rejected for v2 (expects EC-p521 + ML-KEM)"
);
let err = result.unwrap_err();
if let pv::Error::InvalidHkd(msg) = err {
assert_eq!(
msg, "First key must be a EC-p521 key",
"Error message should indicate EC-p521 requirement"
);
} else {
panic!(
"Expected InvalidHkd error for wrong key types, got: {:?}",
err
);
}
}
#[test]
fn test_wrong_second_key_type_v2() {
// Create a file with EC-p521 + EC (wrong) instead of EC-p521 + ML-KEM for v2
let temp_dir = TemporaryDirectory::new().unwrap();
let file_path = temp_dir.path().join("wrong_second_key_v2.hkd");
let mut temp_file = File::create(&file_path).unwrap();
let ec_p521_cert = fs::read_to_string(cert_asset_path("host.crt")).unwrap();
let ec_cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("pv/tests/assets/keys/host.ec.crt");
let ec_cert = fs::read_to_string(&ec_cert_path).unwrap();
write!(temp_file, "{}{}", ec_p521_cert, ec_cert).unwrap();
temp_file.flush().unwrap();
let result = HkdLoader::load_and_verify(
&file_path,
&NoVerifyHkd,
AutoOrExplicit::Explicit(HkdVersion::Hybrid),
);
assert!(
result.is_err(),
"EC key should be rejected as second key for v2 (expects ML-KEM-1024)"
);
let err = result.unwrap_err();
if let pv::Error::InvalidHkd(msg) = err {
assert_eq!(
msg, "Second key must be a ML-KEM 1024 key",
"Error message should indicate ML-KEM-1024 requirement"
);
} else {
panic!(
"Expected InvalidHkd error for wrong second key type, got: {:?}",
err
);
}
}

View File

@@ -0,0 +1,17 @@
[package]
name = "utils_macros"
version = "0.12.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[lib]
proc-macro = true
[dependencies]
syn = { version = "2.0", features = ["full", "extra-traits"] }
quote = "1.0"
proc-macro2 = "1.0"
[dev-dependencies]
clap = { version = "4.6", features = ["derive"] }

View File

@@ -0,0 +1,297 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp.
//! Procedural macros for the utils crate.
//!
//! This crate provides derive macros to reduce boilerplate in enum definitions,
//! particularly for control flags.
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields, Lit, Meta, MetaList};
/// Derive macro for control flag enums.
///
/// This macro generates implementations for `Display`, `IntoEnumIterator`, and `ControlFlagTrait`.
/// It supports the `#[flag(display = "...", value = N)]` attribute to specify custom display
/// strings and discriminant values.
///
/// # Example
///
/// ```
/// use utils_macros::ControlFlag;
///
/// /// Trait for enums that can be iterated over.
/// pub trait IntoEnumIterator: Sized {
/// /// Returns an iterator over all variants of the enum.
/// fn iter() -> impl Iterator<Item = Self>;
/// }
///
/// /// Trait for control flags that provide bit position information.
/// pub trait ControlFlagTrait {
/// /// Returns the bit position for this flag.
/// fn bit_position(self) -> u8;
/// }
///
/// #[derive(ControlFlag)]
/// pub enum PcfV1 {
/// #[flag(display = "Confidential dump support", value = 34)]
/// ConfidentialDump,
///
/// #[flag(display = "V1-specific flag", value = 35)]
/// V1OnlyFlag,
/// }
/// ```
#[proc_macro_derive(ControlFlag, attributes(flag))]
pub fn derive_control_flag(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let variants = match &input.data {
Data::Enum(data) => &data.variants,
_ => panic!("ControlFlag can only be derived for enums"),
};
// Extract variant information
let mut variant_names = Vec::new();
let mut variant_displays = Vec::new();
let mut variant_values = Vec::new();
for variant in variants {
if !matches!(variant.fields, Fields::Unit) {
panic!("ControlFlag only supports unit variants");
}
let variant_name = &variant.ident;
variant_names.push(variant_name);
// Parse the #[flag(...)] attribute
let mut display_str = variant_name.to_string();
let mut value: Option<u8> = None;
for attr in &variant.attrs {
if attr.path().is_ident("flag") {
// Try to parse as MetaList
if let Meta::List(MetaList { tokens, .. }) = &attr.meta {
// Parse the tokens inside the list
let parser = syn::meta::parser(|meta| {
if meta.path.is_ident("display") {
let val = meta.value()?;
let lit: Lit = val.parse()?;
if let Lit::Str(s) = lit {
display_str = s.value();
}
} else if meta.path.is_ident("value") {
let val = meta.value()?;
let lit: Lit = val.parse()?;
if let Lit::Int(i) = lit {
value = Some(i.base10_parse()?);
}
}
Ok(())
});
let _ = syn::parse::Parser::parse2(parser, tokens.clone());
}
}
}
if value.is_none() {
panic!(
"ControlFlag variant {} must have a value attribute",
variant_name
);
}
variant_displays.push(display_str);
variant_values.push(value.unwrap());
}
let expanded = quote! {
impl #name {
/// Returns the bit position value for this flag.
pub const fn flag_value(&self) -> u8 {
match self {
#(Self::#variant_names => #variant_values),*
}
}
}
impl ControlFlagTrait for #name {
fn bit_position(self) -> u8 {
self.flag_value()
}
}
impl std::fmt::Display for #name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
#(Self::#variant_names => #variant_displays),*
}
)
}
}
impl IntoEnumIterator for #name {
fn iter() -> impl Iterator<Item = Self> {
[
#(Self::#variant_names),*
]
.into_iter()
}
}
};
TokenStream::from(expanded)
}
/// Derive `std::fmt::Display` for enums implementing `clap::ValueEnum`.
///
/// This macro generates a `Display` implementation that delegates to
/// `ValueEnum::to_possible_value()`, ensuring that the formatted output
/// matches the CLI representation used by clap (e.g. for help text,
/// completions, and parsing).
///
/// # Behavior
///
/// - Uses the canonical CLI name of each variant (as defined by `#[value(name = "...")]` or the
/// default casing).
/// - Fails at runtime if a variant is marked with `#[value(skip)]` and therefore has no CLI
/// representation.
///
/// # Example
///
/// ```rust
/// use clap::ValueEnum;
/// use utils_macros::ValueEnumDisplay;
///
/// #[derive(ValueEnum, ValueEnumDisplay, Clone)]
/// enum Mode {
/// #[value(name = "very-fast")]
/// Fast,
///
/// #[value(name = "slow")]
/// Slow,
/// }
///
/// assert_eq!(Mode::Fast.to_string(), "very-fast");
/// ```
///
/// # Rationale
///
/// clap requires `Display` for features like `default_value_t`. However,
/// `ValueEnum` already defines the canonical string representation via
/// `to_possible_value()`. This derive avoids duplicating those strings
/// and guarantees consistency between parsing, help output, and display.
///
/// # Panics
///
/// Panics if called on a variant with `#[value(skip)]`, as such variants
/// have no associated CLI representation.
///
/// # See also
///
/// - [`clap::ValueEnum`]
/// - [`clap::builder::PossibleValue`]
#[proc_macro_derive(ValueEnumDisplay)]
pub fn derive_value_enum_display(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let expanded = quote! {
impl std::fmt::Display for #name {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
let value = self
.to_possible_value()
.expect("skipped ValueEnum variant cannot be displayed");
write!(f, "{}", value.get_name())
}
}
};
expanded.into()
}
/// Derives a `std::str::FromStr` implementation for enums implementing
/// [`clap::ValueEnum`].
///
/// This macro generates a `FromStr` implementation that delegates to
/// [`ValueEnum::from_str`], ensuring that parsing behavior is identical
/// to clap's CLI parsing.
///
/// # Behavior
///
/// - Parses input strings using the canonical CLI representation defined by `ValueEnum` (including
/// `#[value(name = "...")]` and aliases).
/// - Supports the same parsing semantics as clap (e.g. case sensitivity, if enabled).
/// - Returns a human-readable error if parsing fails.
///
/// # Example
///
/// ```rust
/// use clap::ValueEnum;
/// use utils_macros::ValueEnumFromStr;
///
/// #[derive(ValueEnum, ValueEnumFromStr, Clone, Debug, PartialEq)]
/// enum Mode {
/// #[value(name = "fast")]
/// Fast,
///
/// #[value(name = "slow")]
/// Slow,
/// }
///
/// assert_eq!("fast".parse::<Mode>().unwrap(), Mode::Fast);
/// assert!("invalid".parse::<Mode>().is_err());
/// ```
///
/// # Rationale
///
/// clap's [`ValueEnum`] trait already defines the canonical mapping
/// between strings and enum variants. This derive avoids duplicating
/// that logic in manual `FromStr` implementations and guarantees that
/// CLI parsing and programmatic parsing remain consistent.
///
/// # Errors
///
/// Returns an error if the input does not match any of the allowed values
/// defined by `ValueEnum`.
///
/// # See also
///
/// - [`clap::ValueEnum`]
/// - [`std::str::FromStr`]
#[proc_macro_derive(ValueEnumFromStr)]
pub fn derive_value_enum_from_str(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let expanded = quote! {
impl std::str::FromStr for #name {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
<Self as clap::ValueEnum>::from_str(s, false).map_err(|_| {
let possible = <Self as clap::ValueEnum>::value_variants()
.iter()
.filter_map(|v| v.to_possible_value())
.map(|v| v.get_name().to_string())
.collect::<Vec<_>>()
.join(", ");
format!("invalid value '{}', expected one of: {}", s, possible)
})
}
}
};
expanded.into()
}

View File

@@ -0,0 +1,247 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp.
//! Integration tests for the ControlFlag derive macro.
mod test_helpers;
use test_helpers::{ControlFlagTrait, IntoEnumIterator};
use utils_macros::ControlFlag;
// Test enum for basic functionality
#[derive(ControlFlag, Debug, Clone, Copy, PartialEq, Eq)]
enum TestFlag {
#[flag(display = "first flag", value = 1)]
First,
#[flag(display = "second flag", value = 2)]
Second,
}
// Test enum with multiple variants
#[derive(ControlFlag, Debug, Clone, Copy, PartialEq, Eq)]
enum MultiFlag {
#[flag(display = "flag 1", value = 10)]
Flag1,
#[flag(display = "flag 2", value = 20)]
Flag2,
#[flag(display = "flag 3", value = 30)]
Flag3,
#[flag(display = "flag 4", value = 40)]
Flag4,
#[flag(display = "flag 5", value = 50)]
Flag5,
}
// Test enum with non-sequential values
#[derive(ControlFlag, Debug, Clone, Copy, PartialEq, Eq)]
enum SparseFlag {
#[flag(display = "low bit", value = 1)]
Low,
#[flag(display = "high bit", value = 63)]
High,
#[flag(display = "middle bit", value = 32)]
Middle,
}
// Test enum with edge case display strings
#[derive(ControlFlag, Debug, Clone, Copy, PartialEq, Eq)]
enum EdgeCaseFlag {
#[flag(display = "simple", value = 1)]
Simple,
#[flag(display = "with spaces and punctuation!", value = 2)]
WithSpaces,
#[flag(display = "UPPERCASE", value = 3)]
Uppercase,
#[flag(display = "with-dashes-and_underscores", value = 4)]
WithDashes,
}
// Test enum for Copy/Clone compatibility
#[derive(ControlFlag, Copy, Clone, Debug, PartialEq, Eq)]
enum CopyableFlag {
#[flag(display = "copyable", value = 1)]
Copyable,
#[flag(display = "another", value = 2)]
Another,
}
#[test]
fn test_basic_derive() {
// Verify the macro successfully derives all required traits
let flag = TestFlag::First;
// Should compile and be accessible
let _ = flag.flag_value();
let _ = flag.bit_position();
let _ = format!("{}", flag);
let _ = TestFlag::iter();
}
#[test]
fn test_display_trait() {
// Verify custom display strings are correctly used
assert_eq!(format!("{}", TestFlag::First), "first flag");
assert_eq!(format!("{}", TestFlag::Second), "second flag");
}
#[test]
fn test_enum_iterator() {
// Verify iteration over all enum variants works correctly
let flags: Vec<TestFlag> = TestFlag::iter().collect();
assert_eq!(flags.len(), 2);
assert_eq!(flags[0], TestFlag::First);
assert_eq!(flags[1], TestFlag::Second);
}
#[test]
fn test_control_flag_trait() {
// Verify bit_position() returns correct values
assert_eq!(TestFlag::First.bit_position(), 1);
assert_eq!(TestFlag::Second.bit_position(), 2);
}
#[test]
fn test_flag_value_method() {
// Verify flag_value() returns correct bit positions
assert_eq!(TestFlag::First.flag_value(), 1);
assert_eq!(TestFlag::Second.flag_value(), 2);
}
#[test]
fn test_multiple_variants() {
// Verify the macro handles enums with many variants
// Test iteration
let flags: Vec<MultiFlag> = MultiFlag::iter().collect();
assert_eq!(flags.len(), 5);
assert_eq!(flags[0], MultiFlag::Flag1);
assert_eq!(flags[1], MultiFlag::Flag2);
assert_eq!(flags[2], MultiFlag::Flag3);
assert_eq!(flags[3], MultiFlag::Flag4);
assert_eq!(flags[4], MultiFlag::Flag5);
// Test bit positions
assert_eq!(MultiFlag::Flag1.bit_position(), 10);
assert_eq!(MultiFlag::Flag2.bit_position(), 20);
assert_eq!(MultiFlag::Flag3.bit_position(), 30);
assert_eq!(MultiFlag::Flag4.bit_position(), 40);
assert_eq!(MultiFlag::Flag5.bit_position(), 50);
// Test display strings
assert_eq!(format!("{}", MultiFlag::Flag1), "flag 1");
assert_eq!(format!("{}", MultiFlag::Flag2), "flag 2");
assert_eq!(format!("{}", MultiFlag::Flag3), "flag 3");
assert_eq!(format!("{}", MultiFlag::Flag4), "flag 4");
assert_eq!(format!("{}", MultiFlag::Flag5), "flag 5");
}
#[test]
fn test_non_sequential_values() {
// Verify the macro handles non-sequential bit position values
assert_eq!(SparseFlag::Low.bit_position(), 1);
assert_eq!(SparseFlag::High.bit_position(), 63);
assert_eq!(SparseFlag::Middle.bit_position(), 32);
// Verify iteration order matches declaration order
let flags: Vec<SparseFlag> = SparseFlag::iter().collect();
assert_eq!(flags.len(), 3);
assert_eq!(flags[0], SparseFlag::Low);
assert_eq!(flags[1], SparseFlag::High);
assert_eq!(flags[2], SparseFlag::Middle);
}
#[test]
fn test_display_string_edge_cases() {
// Verify various display string formats work correctly
assert_eq!(format!("{}", EdgeCaseFlag::Simple), "simple");
assert_eq!(
format!("{}", EdgeCaseFlag::WithSpaces),
"with spaces and punctuation!"
);
assert_eq!(format!("{}", EdgeCaseFlag::Uppercase), "UPPERCASE");
assert_eq!(
format!("{}", EdgeCaseFlag::WithDashes),
"with-dashes-and_underscores"
);
}
#[test]
fn test_trait_bounds() {
// Verify generated implementations work with common trait bounds
// Function that requires Display
fn requires_display<T: std::fmt::Display>(flag: T) -> String {
format!("{}", flag)
}
// Function that requires IntoEnumIterator
fn requires_iterator<T: IntoEnumIterator>() -> Vec<T> {
T::iter().collect()
}
// Test with TestFlag
let result = requires_display(TestFlag::First);
assert_eq!(result, "first flag");
let flags: Vec<TestFlag> = requires_iterator();
assert_eq!(flags.len(), 2);
}
#[test]
fn test_copy_clone_compatibility() {
// Verify the macro works with Copy and Clone derives
let flag1 = CopyableFlag::Copyable;
let flag2 = flag1; // Copy
let flag3 = flag1; // Clone
assert_eq!(flag1, flag2);
assert_eq!(flag1, flag3);
// Verify all methods still work
assert_eq!(flag1.bit_position(), 1);
assert_eq!(flag2.flag_value(), 1);
assert_eq!(format!("{}", flag3), "copyable");
}
#[test]
fn test_iterator_multiple_calls() {
// Verify iterator can be called multiple times
let iter1: Vec<TestFlag> = TestFlag::iter().collect();
let iter2: Vec<TestFlag> = TestFlag::iter().collect();
assert_eq!(iter1, iter2);
assert_eq!(iter1.len(), 2);
}
#[test]
fn test_flag_value_const() {
// Verify flag_value() can be used in const contexts
const FLAG_VALUE: u8 = TestFlag::First.flag_value();
assert_eq!(FLAG_VALUE, 1);
}
#[test]
fn test_all_variants_unique_values() {
// Verify all variants have unique bit positions
let flags: Vec<TestFlag> = TestFlag::iter().collect();
let values: Vec<u8> = flags.iter().map(|f| f.bit_position()).collect();
// Check uniqueness
for i in 0..values.len() {
for j in (i + 1)..values.len() {
assert_ne!(values[i], values[j], "Duplicate bit position found");
}
}
}
#[test]
fn test_display_consistency() {
// Verify Display is consistent across multiple calls
let flag = TestFlag::First;
let display1 = format!("{}", flag);
let display2 = format!("{}", flag);
assert_eq!(display1, display2);
assert_eq!(display1, "first flag");
}

View File

@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp.
//! Helper traits and utilities for testing the ControlFlag derive macro.
/// Trait for control flags that provide bit position information.
pub trait ControlFlagTrait {
/// Returns the bit position for this flag.
fn bit_position(self) -> u8;
}
/// Trait for enums that can be iterated over.
pub trait IntoEnumIterator: Sized {
/// Returns an iterator over all variants of the enum.
fn iter() -> impl Iterator<Item = Self>;
}

View File

@@ -515,6 +515,7 @@ CMDS="${CMDS}\
:lspci -vvv\
:lstape\
:smc_dbg\
:zpcimon --module-info --smart-log-raw\
"
# block device and other scsi device commands (in non alphabetical order)
@@ -629,7 +630,7 @@ NETWORK_CMDS="ip -br a\
:nstat -az\
:openssl engine # deprecated in OpenSSL 3.n\
:openssl list --providers 2>/dev/null # redirect help on older systems\
:opticsmon --module-info\
:opticsmon --module-info\ # Now called zpcimon but kept for compatibility
:route -n\
"

View File

@@ -4,7 +4,7 @@ SYSTEM_UNITS = ttyrun-getty@.service iucvtty-login@.service \
cpacfstatsd.service cpuplugd.service \
dumpconf.service cpi.service \
mon_fsstatd.service mon_procd.service \
opticsmon.service
zpcimon.service
all:
@@ -19,6 +19,9 @@ install: system_units
$(INSTALL) -g $(GROUP) -o $(OWNER) \
-m 644 $$unit $(DESTDIR)$(SYSTEMDSYSTEMUNITDIR) ; \
done
# Make the legacy opticsmon.service a link to zpcimon.service
ln -sf zpcimon.service \
$(DESTDIR)$(SYSTEMDSYSTEMUNITDIR)/opticsmon.service
else

View File

@@ -1,5 +1,5 @@
#
# Systemd unit for mon_fsstatd (monitor z/VM guest file systems)
# Systemd unit for zpcimon PCI NIC optical module monitoring
#
# Copyright IBM Corp. 2024
#
@@ -9,11 +9,11 @@
[Unit]
Description=Monitor health of directly attached PCI NIC optical modules
# Documentation=man:opticsmon
# Documentation=man:zpcimon
[Service]
Type=exec
ExecStart=@usrsbin_path@/opticsmon --send-report --monitor --interval 86400
ExecStart=@usrsbin_path@/zpcimon --send-report --monitor --format jsonl --interval 86400
KillMode=control-group
[Install]

View File

@@ -56,6 +56,11 @@ Sort and display the data according to given FIELD value. Valid values for
FIELD are "nr" for partition number, "lpar" for partition name, and "sum" for
total partition increments.
.TP
.BR "\-p <SUBSTRING>" " or " "\-\-partition-filter=<SUBSTRING>"
Display only the partitions with names that contain SUBSTRING.
Matching is case-insensitive.
.TP
.BR "\-i" " or " "\-\-ascii"
Use only ASCII characters to render views.

View File

@@ -46,6 +46,7 @@ static struct zmemtopo_globals {
unsigned int sort_field;
unsigned int ascii;
unsigned int fmt_specified;
char *partition_filter;
enum util_fmt_t format;
enum util_fmt_flags_t fmt_flags;
} g;
@@ -62,15 +63,11 @@ static void parse_nesting_level(char *arg)
static void parse_sort_field(char *arg)
{
char *s;
s = util_strdup(arg);
util_strstrip(s);
if (strcasecmp(s, "nr") == 0)
if (strcasecmp(arg, "nr") == 0)
g.sort_field = SORT_NR;
else if (strcasecmp(s, "lpar") == 0)
else if (strcasecmp(arg, "lpar") == 0)
g.sort_field = SORT_NAME;
else if (strcasecmp(s, "size") == 0)
else if (strcasecmp(arg, "size") == 0)
g.sort_field = SORT_SIZE;
else
errx(EXIT_FAILURE, "%s is not a valid sort field option", arg);
@@ -120,6 +117,9 @@ static void parse_args(int argc, char *argv[])
case 's':
parse_sort_field(optarg);
break;
case 'p':
g.partition_filter = optarg;
break;
case 'i':
g.ascii = 1;
break;
@@ -313,7 +313,6 @@ static void topology_entries_add_entry(struct topology_entry *entry,
static void partition_set_name(struct partition *part, char *pname)
{
ebcdic_to_ascii(pname, part->part_name, LPAR_NAME_LEN);
util_strstrip(part->part_name);
}
static void partition_add_entry(struct partition *part,
@@ -488,6 +487,22 @@ static void partition_list_sort(struct partitions *parts)
}
}
static int partition_filter_matches(struct partition *part)
{
int result;
if (!part)
return 0;
if (!g.partition_filter)
return 1;
if (!strlen(g.partition_filter))
return 1;
result = 0;
if (strcasestr(part->part_name, g.partition_filter))
result = 1;
return result;
}
static unsigned int find_entry_cell_size(struct partitions *parts)
{
unsigned int max_digit, max_increment;
@@ -545,8 +560,8 @@ static void table_print_row(char **buf, struct partition *cur,
unsigned int i, s_padding;
s_padding = vdata->entry_len >= SUM_PAD ? vdata->entry_len : SUM_PAD;
concat_w_padding(buf, LPAR_NO_LEN, 0, "%2d", cur->part_nr);
concat_w_padding(buf, LPAR_NAME_LEN, 0, "%s", cur->part_name);
concat_w_padding(buf, LPAR_NO_LEN, 1, "%2d", cur->part_nr);
concat_w_padding(buf, LPAR_NAME_LEN, 1, "%s", cur->part_name);
concat_w_padding(buf, s_padding, 0, "%lu", cur->increment_total);
entries = &cur->entries[g.nesting_level - 1];
for (i = 0; i < entries->count; i++) {
@@ -584,8 +599,8 @@ static void table_print_header(char **buf, struct view_data *vdata)
}
util_concatf(buf, "\n");
}
util_concatf(buf, "%*s", LPAR_NO_LEN, "NR");
util_concatf(buf, "%*s", LPAR_NAME_LEN, "LPAR");
util_concatf(buf, "%-*s", LPAR_NO_LEN, "NR");
util_concatf(buf, "%-*s", LPAR_NAME_LEN, "LPAR");
util_concatf(buf, "%*s\n", s_padding, "SUM");
}
@@ -602,8 +617,11 @@ static void table_print(struct partitions *parts)
vdata->entry_len = find_entry_cell_size(parts);
partition_list_calculate_level_lengths(parts, vdata);
table_print_header(table, vdata);
util_list_iterate(parts->list, cur)
util_list_iterate(parts->list, cur) {
if (!partition_filter_matches(cur))
continue;
table_print_row(table, cur, vdata);
}
printf("%s\n", *table);
printf("Increment size: %lu%s\n", unit.size / unit.scale, unit.suffix);
free(vdata);
@@ -721,6 +739,8 @@ static void tree_create(char **tree, struct partitions *parts,
unit = vdata->unit;
tree_create_header(tree, vdata);
util_list_iterate(parts->list, cur) {
if (!partition_filter_matches(cur))
continue;
util_concatf(tree, "%-*s", vdata->cell_len, cur->part_name);
part_size = cur->increment_total * unit.size / unit.scale;
concat_w_padding(tree, vdata->entry_len, 0, "%u%s", part_size,
@@ -742,7 +762,9 @@ static unsigned int is_increment_at(struct partitions *parts,
else
next = util_list_next(parts->list, cur);
for (; next; next = util_list_next(parts->list, next)) {
if (next->entries[level - 1].increments[idx])
if (!next->entries[level - 1].increments[idx])
continue;
if (partition_filter_matches(next))
return 1;
}
return 0;
@@ -777,6 +799,8 @@ static void rtree_print_parts(char **buf, struct partitions *parts,
unit = vdata->unit;
flag_idx = (g.max_level - level) + 1;
util_list_iterate(parts->list, cur) {
if (!partition_filter_matches(cur))
continue;
entries = &cur->entries[level - 1];
if (!entries->increments[idx])
continue;
@@ -794,6 +818,21 @@ static void rtree_print_parts(char **buf, struct partitions *parts,
}
}
static unsigned int rtree_is_last_entry(struct partitions *parts, unsigned int idx,
unsigned int level, unsigned int end)
{
unsigned int tidx;
if (g.partition_filter && !g.tree_full) {
for (tidx = idx + 1; tidx < end; tidx++) {
if (is_increment_at(parts, NULL, level, tidx))
return 0;
}
return 1;
}
return (idx + 1) == end;
}
static void rtree_level_to_part(char **buf, struct partitions *parts,
unsigned int step, unsigned int level,
struct view_data *vdata)
@@ -812,7 +851,8 @@ static void rtree_level_to_part(char **buf, struct partitions *parts,
if (!is_increment_at(parts, NULL, level, idx) && !g.tree_full)
continue;
indent = 0;
vdata->end_flag[flag_idx] = (idx + 1) == end;
vdata->end_flag[flag_idx] = rtree_is_last_entry(parts, idx,
level, end);
if (level != g.max_level)
indent = tree_add_indent(buf, level, vdata->end_flag);
concat_w_padding(buf, vdata->cell_len - indent, 1, "LEVEL%u_%u",

View File

@@ -68,9 +68,9 @@ struct diag310_t_hdr {
} __packed;
struct diag310_p_hdr {
char pname[8]; /* partition name */
uint8_t pn; /* partition number */
uint8_t tie; /* count of entries following this header */
char pname[LPAR_NAME_LEN]; /* partition name */
uint8_t pn; /* partition number */
uint8_t tie; /* count of entries following this header */
uint16_t reserved1;
uint32_t reserved2;
} __packed;

View File

@@ -36,6 +36,10 @@ static struct util_opt opt_vec[] = {
.option = { "sort", required_argument, NULL, 's' },
.argument = "FIELD",
.desc = "Sort view by FIELD (nr, lpar, size)"
}, {
.option = { "partition-filter", required_argument, NULL, 'p' },
.argument = "SUBSTRING",
.desc = "Filter partitions by name substring (case-insensitive)"
}, {
.option = { "ascii", no_argument, NULL, 'i' },
.desc = "Use only ASCII characters",

Some files were not shown because too many files have changed in this diff Show More