From 16610a211fdb6dd8456edff9b5f7891014e8cae3 Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Fri, 5 Apr 2024 13:28:04 +0200 Subject: [PATCH] rust: pvattest-Rust Add a CLI compatible Rust implementation of pvattest-C. - All (non-experimental) options are supported and work exactly as in the C implementation. For some options/parameters new variants are available. - `perform` now also accepts positional arguments, while keep accepting -i and -o that was mandatory in the C implementation. - `version` may also be a command instead of an option now. - -V is deprecated - -v increases verbosity instead of showing the version - all experimental options are dropped Acked-by: Qi Feng Huo Acked-by: Marc Hartmayer Signed-off-by: Steffen Eiden --- rust/Cargo.lock | 15 + rust/Cargo.toml | 1 + rust/pvattest/Cargo.toml | 17 + rust/pvattest/README.md | 261 ++++++ rust/pvattest/man/pvattest-create.1 | 132 +++ rust/pvattest/man/pvattest-perform.1 | 71 ++ rust/pvattest/man/pvattest-verify.1 | 124 +++ rust/pvattest/man/pvattest.1 | 114 +++ rust/pvattest/src/cli.rs | 257 +++++ rust/pvattest/src/cmd.rs | 33 + rust/pvattest/src/cmd/create.rs | 67 ++ rust/pvattest/src/cmd/perform.rs | 54 ++ rust/pvattest/src/cmd/verify.rs | 124 +++ rust/pvattest/src/exchange.rs | 884 ++++++++++++++++++ rust/pvattest/src/main.rs | 54 ++ .../tests/assets/exp/exchange/add_req.bin | Bin 0 -> 80 bytes .../tests/assets/exp/exchange/add_resp.bin | Bin 0 -> 192 bytes .../tests/assets/exp/exchange/full.bin | Bin 0 -> 448 bytes .../tests/assets/exp/exchange/full_req.bin | Bin 0 -> 80 bytes .../tests/assets/exp/exchange/full_resp.bin | Bin 0 -> 448 bytes .../tests/assets/exp/exchange/min_req.bin | Bin 0 -> 80 bytes .../tests/assets/exp/exchange/min_resp.bin | Bin 0 -> 160 bytes .../tests/assets/exp/exchange/user_req.bin | Bin 0 -> 336 bytes .../tests/assets/exp/exchange/user_resp.bin | Bin 0 -> 416 bytes 24 files changed, 2208 insertions(+) create mode 100644 rust/pvattest/Cargo.toml create mode 100644 rust/pvattest/README.md create mode 100644 rust/pvattest/man/pvattest-create.1 create mode 100644 rust/pvattest/man/pvattest-perform.1 create mode 100644 rust/pvattest/man/pvattest-verify.1 create mode 100644 rust/pvattest/man/pvattest.1 create mode 100644 rust/pvattest/src/cli.rs create mode 100644 rust/pvattest/src/cmd.rs create mode 100644 rust/pvattest/src/cmd/create.rs create mode 100644 rust/pvattest/src/cmd/perform.rs create mode 100644 rust/pvattest/src/cmd/verify.rs create mode 100644 rust/pvattest/src/exchange.rs create mode 100644 rust/pvattest/src/main.rs create mode 100644 rust/pvattest/tests/assets/exp/exchange/add_req.bin create mode 100644 rust/pvattest/tests/assets/exp/exchange/add_resp.bin create mode 100644 rust/pvattest/tests/assets/exp/exchange/full.bin create mode 100644 rust/pvattest/tests/assets/exp/exchange/full_req.bin create mode 100644 rust/pvattest/tests/assets/exp/exchange/full_resp.bin create mode 100644 rust/pvattest/tests/assets/exp/exchange/min_req.bin create mode 100644 rust/pvattest/tests/assets/exp/exchange/min_resp.bin create mode 100644 rust/pvattest/tests/assets/exp/exchange/user_req.bin create mode 100644 rust/pvattest/tests/assets/exp/exchange/user_resp.bin diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 9227493c..1e48424b 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -413,6 +413,21 @@ dependencies = [ "utils", ] +[[package]] +name = "pvattest" +version = "0.10.0" +dependencies = [ + "anyhow", + "byteorder", + "clap", + "log", + "s390_pv", + "serde", + "serde_yaml", + "utils", + "zerocopy", +] + [[package]] name = "pvsecret" version = "0.10.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 22b3d448..0657649a 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -3,6 +3,7 @@ members = [ "pv", "pv_core", "pvapconfig", + "pvattest", "pvsecret", "utils", ] diff --git a/rust/pvattest/Cargo.toml b/rust/pvattest/Cargo.toml new file mode 100644 index 00000000..3ad2a019 --- /dev/null +++ b/rust/pvattest/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "pvattest" +version = "0.10.0" +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow = { version = "1.0.70", features = ["std"] } +byteorder = "1.3" +clap = { version ="4.1", features = ["derive", "wrap_help"]} +log = { version = "0.4.6", features = ["std", "release_max_level_debug"] } +serde_yaml = "0.9" +serde = { version = "1.0.139", features = ["derive"]} +zerocopy = { version="0.7", features = ["derive"] } + +pv = { path = "../pv", package = "s390_pv" } +utils = { path = "../utils" } diff --git a/rust/pvattest/README.md b/rust/pvattest/README.md new file mode 100644 index 00000000..504c2601 --- /dev/null +++ b/rust/pvattest/README.md @@ -0,0 +1,261 @@ + +# pvattest +## Synopsis +`pvattest [OPTIONS] ` +## Description +create, perform, and verify attestation measurements Create, perform, and verify +attestation measurements for IBM Secure Execution guest systems. +## Commands Overview +- **create** +
    +Create an attestation measurement request +
+ +- **perform** +
    +Send the attestation request to the Ultravisor +
+ +- **verify** +
    +Verify an attestation response +
+ +- **version** +
    +Print version information and exit +
+ +## Options + +`-v`, `--verbose` +
    +Provide more detailed output +
+ + +`--version` +
    +Print version information and exit +
+ + +`-h`, `--help` +
    +Print help (see a summary with '-h') +
+ + +## pvattest create +### Synopsis +`pvattest create [OPTIONS] --host-key-document --output --arpk <--no-verify|--cert >` +### Description +Create an attestation measurement request Create attestation measurement +requests to attest an IBM Secure Execution guest. Only build attestation +requests in a trusted environment such as your Workstation. To avoid +compromising the attestation do not publish the attestation request protection +key and shred it after verification. Every 'create' will generate a new, random +protection key. +### Options + +`-k`, `--host-key-document ` +
    +Use FILE as a host-key document. Can be specified multiple times and must be +used at least once. +
+ + +`--no-verify` +
    +Disable the host-key document verification. Does not require the host-key +documents to be valid. Do not use for a production request unless you verified +the host-key document beforehand. +
+ + +`-C`, `--cert ` +
    +Use FILE as a certificate to verify the host key or keys. The certificates are +used to establish a chain of trust for the verification of the host-key +documents. Specify this option twice to specify the IBM Z signing key and the +intermediate CA certificate (signed by the root CA). +
+ + +`--crl ` +
    +Use FILE as a certificate revocation list. The list is used to check whether a +certificate of the chain of trust is revoked. Specify this option multiple times +to use multiple CRLs. +
+ + +`--offline` +
    +Make no attempt to download CRLs +
+ + +`--root-ca ` +
    +Use FILE as the root-CA certificate for the verification. If omitted, the system +wide-root CAs installed on the system are used. Use this only if you trust the +specified certificate. +
+ + +`-o`, `--output ` +
    +Write the generated request to FILE +
+ + +`--arpk ` +
    +Save the protection key as unencrypted GCM-AES256 key in FILE Do not publish +this key, otherwise your attestation is compromised. +
+ + +`--add-data ` +
    +Specify-additional data for the request. Additional data is provided by the +Ultravisor and returned during the attestation request and is covered by the +attestation measurement. Can be specified multiple times. Optional. + Possible values: + - **phkh-img**: Request the public host-key-hash of the key that decrypted the SE-image as additional-data + - **phkh-att**: Request the public host-key-hash of the key that decrypted the attestation request as additional-data +
+ + +`-v`, `--verbose` +
    +Provide more detailed output +
+ + +`-h`, `--help` +
    +Print help (see a summary with '-h') +
+ + +## pvattest perform +### Synopsis +`pvattest perform [OPTIONS] [INPUT] [OUTPUT]` +### Description +Send the attestation request to the Ultravisor Run a measurement of this system +through ’/dev/uv’. This device must be accessible and the attestation +Ultravisor facility must be present. The input must be an attestation request +created with ’pvattest create’. Output will contain the original request and +the response from the Ultravisor. +### Arguments + +`` +
    +Specify the request to be sent +
+ + +`` +
    +Write the result to FILE +
+ + +### Options + +`-u`, `--user-data ` +
    +Provide up to 256 bytes of user input User-data is arbitrary user-defined data +appended to the Attestation measurement. It is verified during the Attestation +measurement verification. May be any arbitrary data, as long as it is less or +equal to 256 bytes +
+ + +`-v`, `--verbose` +
    +Provide more detailed output +
+ + +`-h`, `--help` +
    +Print help (see a summary with '-h') +
+ + +## pvattest verify +### Synopsis +`pvattest verify [OPTIONS] --input --hdr --arpk ` +### Description +Verify an attestation response Verify that a previously generated attestation +measurement of an IBM Secure Execution guest is as expected. Only verify +attestation requests in a trusted environment, such as your workstation. Input +must contain the response as produced by ’pvattest perform’. The protection +key must be the one that was used to create the request by ’pvattest create’. +Shred the protection key after the verification. The header must be the IBM +Secure Execution header of the image that was attested during ’pvattest +perform’ +### Options + +`-i`, `--input ` +
    +Specify the attestation request to be verified +
+ + +`-o`, `--output ` +
    +Specify the output for the verification result +
+ + +`--hdr ` +
    +Specifies the header of the guest image. Can be an IBM Secure Execution image +created by genprotimg or an extracted IBM Secure Execution header. The header +must start at a page boundary. +
+ + +`--arpk ` +
    +Use FILE as the protection key to decrypt the request Do not publish this key, +otherwise your attestation is compromised. Delete this key after verification. +
+ + +`--format ` +
    +Define the output format + Default value: 'yaml' + Possible values: + - **yaml**: Use yaml format +
+ + +`-u`, `--user-data ` +
    +Write the user data to the FILE if any. Writes the user data, if the response +contains any, to FILE The user-data is part of the attestation measurement. If +the user-data is written to FILE the user-data was part of the measurement and +verified. Emits a warning if the response contains no user-data +
+ + +`-v`, `--verbose` +
    +Provide more detailed output +
+ + +`-h`, `--help` +
    +Print help (see a summary with '-h') +
diff --git a/rust/pvattest/man/pvattest-create.1 b/rust/pvattest/man/pvattest-create.1 new file mode 100644 index 00000000..bf6f0020 --- /dev/null +++ b/rust/pvattest/man/pvattest-create.1 @@ -0,0 +1,132 @@ +.\" Copyright 2024 IBM Corp. +.\" s390-tools is free software; you can redistribute it and/or modify +.\" it under the terms of the MIT license. See LICENSE for details. +.\" + +.TH pvattest-create 1 "2024-05-15" "s390-tools" "Attestation Manual" +.nh +.ad l +.SH NAME +\fBpvattest create\fP - Create an attestation measurement request +\fB +.SH SYNOPSIS +.nf +.fam C +pvattest create [OPTIONS] --host-key-document --output --arpk <--no-verify|--cert > +.fam C +.fi +.SH DESCRIPTION +Create attestation measurement requests to attest an IBM Secure Execution guest. +Only build attestation requests in a trusted environment such as your +Workstation. To avoid compromising the attestation do not publish the +attestation request protection key and shred it after verification. Every +'create' will generate a new, random protection key. +.SH OPTIONS +.PP +\-k, \-\-host-key-document +.RS 4 +Use FILE as a host-key document. Can be specified multiple times and must be +used at least once. +.RE +.RE +.PP +\-\-no-verify +.RS 4 +Disable the host-key document verification. Does not require the host-key +documents to be valid. Do not use for a production request unless you verified +the host-key document beforehand. +.RE +.RE +.PP +\-C, \-\-cert +.RS 4 +Use FILE as a certificate to verify the host key or keys. The certificates are +used to establish a chain of trust for the verification of the host-key +documents. Specify this option twice to specify the IBM Z signing key and the +intermediate CA certificate (signed by the root CA). +.RE +.RE +.PP +\-\-crl +.RS 4 +Use FILE as a certificate revocation list. The list is used to check whether a +certificate of the chain of trust is revoked. Specify this option multiple times +to use multiple CRLs. +.RE +.RE +.PP +\-\-offline +.RS 4 +Make no attempt to download CRLs. +.RE +.RE +.PP +\-\-root-ca +.RS 4 +Use FILE as the root-CA certificate for the verification. If omitted, the system +wide-root CAs installed on the system are used. Use this only if you trust the +specified certificate. +.RE +.RE +.PP +\-o, \-\-output +.RS 4 +Write the generated request to FILE. +.RE +.RE +.PP +\-\-arpk +.RS 4 +Save the protection key as unencrypted GCM-AES256 key in FILE Do not publish +this key, otherwise your attestation is compromised. +.RE +.RE +.PP +\-\-add-data +.RS 4 +Specify-additional data for the request. Additional data is provided by the +Ultravisor and returned during the attestation request and is covered by the +attestation measurement. Can be specified multiple times. Optional. + +Possible values: +.RS 4 +- \fBphkh-img\fP: Request the public host-key-hash of the key that decrypted the SE-image as additional-data. + +- \fBphkh-att\fP: Request the public host-key-hash of the key that decrypted the attestation request as additional-data. + +.RE +.RE +.PP +\-v, \-\-verbose +.RS 4 +Provide more detailed output. +.RE +.RE +.PP +\-h, \-\-help +.RS 4 +Print help (see a summary with '-h'). +.RE +.RE + +.SH EXAMPLES +Create an attestation request with the protection key 'arp.key', write the request to 'arcb.bin', and verify the host-key document using the CA-signed key 'DigiCertCA.crt' and the intermediate key 'IbmSigningKey.crt'. +.PP +.nf +.fam C + $ pvattest create \-k hkd.crt -\-\arpk arp.key \-o attreq.bin \-\-cert DigiCertCA.crt \-\-cert IbmSigningKey.crt + +.fam T +.fi +Create an attestation request with the protection key 'arp.key', write the request to 'arcb.bin', verify the host-key document using the CA-signed key 'DigiCertCA.crt' and the intermediate key 'IbmSigningKey.crt', and instead of downloading the certificate revocation list use certificate revocation lists 'DigiCertCA.crl', 'IbmSigningKey.crl', and 'rootCA.crl'. +.PP +.nf +.fam C + $ pvattest create \-k hkd.crt \-\-arpk arp.key \-o attreq.bin \-\-cert DigiCertCA.crt \-\-cert IbmSigningKey.crt \-\-offline \-\-crl DigiCertCA.crl \-\-crl IbmSigningKey.crl \-\-crl rootCA.crl + + +.fam T +.fi +.SH "SEE ALSO" +.sp +\fBpvattest\fR(1) diff --git a/rust/pvattest/man/pvattest-perform.1 b/rust/pvattest/man/pvattest-perform.1 new file mode 100644 index 00000000..ddd0a882 --- /dev/null +++ b/rust/pvattest/man/pvattest-perform.1 @@ -0,0 +1,71 @@ +.\" Copyright 2024 IBM Corp. +.\" s390-tools is free software; you can redistribute it and/or modify +.\" it under the terms of the MIT license. See LICENSE for details. +.\" + +.TH pvattest-perform 1 "2024-05-15" "s390-tools" "Attestation Manual" +.nh +.ad l +.SH NAME +\fBpvattest perform\fP - Send the attestation request to the Ultravisor +\fB +.SH SYNOPSIS +.nf +.fam C +pvattest perform [OPTIONS] [INPUT] [OUTPUT] +.fam C +.fi +.SH DESCRIPTION +Run a measurement of this system through ’/dev/uv’. This device must be +accessible and the attestation Ultravisor facility must be present. The input +must be an attestation request created with ’pvattest create’. Output will +contain the original request and the response from the Ultravisor. +.SH OPTIONS +.PP + +.RS 4 +Specify the request to be sent. +.RE +.RE +.PP + +.RS 4 +Write the result to FILE. +.RE +.RE + +.PP +\-u, \-\-user-data +.RS 4 +Provide up to 256 bytes of user input User-data is arbitrary user-defined data +appended to the Attestation measurement. It is verified during the Attestation +measurement verification. May be any arbitrary data, as long as it is less or +equal to 256 bytes +.RE +.RE +.PP +\-v, \-\-verbose +.RS 4 +Provide more detailed output. +.RE +.RE +.PP +\-h, \-\-help +.RS 4 +Print help (see a summary with '-h'). +.RE +.RE + +.SH EXAMPLES +Perform an attestation measurement with the attestation request 'attreq.bin' and write the output to 'attresp.bin'. +.PP +.nf +.fam C + $ pvattest perform attreq.bin attresp.bin + + +.fam T +.fi +.SH "SEE ALSO" +.sp +\fBpvattest\fR(1) diff --git a/rust/pvattest/man/pvattest-verify.1 b/rust/pvattest/man/pvattest-verify.1 new file mode 100644 index 00000000..0a527b2a --- /dev/null +++ b/rust/pvattest/man/pvattest-verify.1 @@ -0,0 +1,124 @@ +.\" Copyright 2024 IBM Corp. +.\" s390-tools is free software; you can redistribute it and/or modify +.\" it under the terms of the MIT license. See LICENSE for details. +.\" + +.TH pvattest-verify 1 "2024-05-15" "s390-tools" "Attestation Manual" +.nh +.ad l +.SH NAME +\fBpvattest verify\fP - Verify an attestation response +\fB +.SH SYNOPSIS +.nf +.fam C +pvattest verify [OPTIONS] --input --hdr --arpk +.fam C +.fi +.SH DESCRIPTION +Verify that a previously generated attestation measurement of an IBM Secure +Execution guest is as expected. Only verify attestation requests in a trusted +environment, such as your workstation. Input must contain the response as +produced by ’pvattest perform’. The protection key must be the one that was +used to create the request by ’pvattest create’. Shred the protection key +after the verification. The header must be the IBM Secure Execution header of +the image that was attested during ’pvattest perform’ +.SH OPTIONS +.PP +\-i, \-\-input +.RS 4 +Specify the attestation request to be verified. +.RE +.RE +.PP +\-o, \-\-output +.RS 4 +Specify the output for the verification result. +.RE +.RE +.PP +\-\-hdr +.RS 4 +Specifies the header of the guest image. Can be an IBM Secure Execution image +created by genprotimg or an extracted IBM Secure Execution header. The header +must start at a page boundary. +.RE +.RE +.PP +\-\-arpk +.RS 4 +Use FILE as the protection key to decrypt the request Do not publish this key, +otherwise your attestation is compromised. Delete this key after verification. +.RE +.RE +.PP +\-\-format +.RS 4 +Define the output format. +[default: 'yaml'] + +Possible values: +.RS 4 +- \fByaml\fP: Use yaml format. + +.RE +.RE +.PP +\-u, \-\-user-data +.RS 4 +Write the user data to the FILE if any. Writes the user data, if the response +contains any, to FILE The user-data is part of the attestation measurement. If +the user-data is written to FILE the user-data was part of the measurement and +verified. Emits a warning if the response contains no user-data +.RE +.RE +.PP +\-v, \-\-verbose +.RS 4 +Provide more detailed output. +.RE +.RE +.PP +\-h, \-\-help +.RS 4 +Print help (see a summary with '-h'). +.RE +.RE + +.SH EXIT STATUS +.TP 8 +.B 0 - Attestation Verified +Attesatation measurement verified successfully. Measured guest is in Secure Execution mode. +.RE + +.TP 8 +.B 1 - Program Error +Something went wrong during the local calculation or receiving of the measurement value. Refer to the error message. +.RE + +.TP 8 +.B 2 - Attestation NOT Verified +Attesation measurement calculation does not match the received value. Measured guest is very likely not in Secure Execution mode. +.RE +.SH EXAMPLES +To verify a measurement in 'measurement.bin' with the protection key 'arp.kep' and SE-guest header 'se_guest.hdr'. +.PP +.nf +.fam C + $ pvattest verify --input attresp.bin --arpk arp.key --hdr se_guest.hdr + +.fam T +.fi +If the verification was successful the program exists with zero. +If the verification failed it exists with 2 and prints the following to stderr: +.PP +.nf +.fam C + ERROR: Attestation measurement verification failed: + Calculated and received attestation measurement are not the same. + +.fam T +.fi +.SH "SEE ALSO" +.sp +\fBpvattest\fR(1) diff --git a/rust/pvattest/man/pvattest.1 b/rust/pvattest/man/pvattest.1 new file mode 100644 index 00000000..752ecee0 --- /dev/null +++ b/rust/pvattest/man/pvattest.1 @@ -0,0 +1,114 @@ +.\" Copyright 2024 IBM Corp. +.\" s390-tools is free software; you can redistribute it and/or modify +.\" it under the terms of the MIT license. See LICENSE for details. +.\" + +.TH pvattest 1 "2024-05-15" "s390-tools" "Attestation Manual" +.nh +.ad l +.SH NAME +\fBpvattest\fP - create, perform, and verify attestation measurements +\fB +.SH SYNOPSIS +.nf +.fam C +pvattest [OPTIONS] +.fam C +.fi +.SH DESCRIPTION +Create, perform, and verify attestation measurements for IBM Secure Execution +guest systems. +.SH "PVATTEST COMMANDS" +.PP + +\fBcreate\fR +.RS 4 +Create an attestation measurement request +.RE + +.PP + +\fBperform\fR +.RS 4 +Send the attestation request to the Ultravisor +.RE + +.PP + +\fBverify\fR +.RS 4 +Verify an attestation response +.RE + +.PP + +\fBversion\fR +.RS 4 +Print version information and exit +.RE + +.SH OPTIONS +.PP +\-v, \-\-verbose +.RS 4 +Provide more detailed output. +.RE +.RE +.PP +\-\-version +.RS 4 +Print version information and exit. +.RE +.RE +.PP +\-h, \-\-help +.RS 4 +Print help (see a summary with '-h'). +.RE +.RE + +.SH EXAMPLES +For details refer to the man page of the command. +.PP +Create the request on a trusted system. +.PP +.nf +.fam C + trusted:~$ pvattest create \-k hkd.crt \-\-cert CA.crt \-\-cert ibmsk.crt \-\-arpk arp.key \-o attreq.bin + +.fam T +.fi +On the SE-guest, \fIperform\fP the attestation. +.PP +.nf +.fam C + seguest:~$ pvattest perform attreq.bin attresp.bin + +.fam T +.fi +On a trusted system, \fIverify\fP that the response is correct. Here, the protection key from the creation and the SE-guest’s header is used to \fIverify\fP the measurement. +.PP +.nf +.fam C + trusted:~$ pvattest verify \-i attresp.bin \-\-arpk arp.key \-\-hdr se_guest.hdr + trusted:~$ echo $? + 0 + +.fam T +.fi + +If the measurements do not match \fBpvattest\fP exits with code 2 and emits an error message. The SE-guest attestation failed. +.PP +.nf +.fam C + trusted:~$ pvattest verify \-i wrongresp.bin \-\-arpk arp.key \-\-hdr se_guest.hdr + ERROR: Attestation measurement verification failed: + Calculated and received attestation measurement are not the same. + trusted:~$ echo $? + 2 + +.fam T +.fi +.SH "SEE ALSO" +.sp +\fBpvattest-create\fR(1) \fBpvattest-perform\fR(1) \fBpvattest-verify\fR(1) diff --git a/rust/pvattest/src/cli.rs b/rust/pvattest/src/cli.rs new file mode 100644 index 00000000..6582331a --- /dev/null +++ b/rust/pvattest/src/cli.rs @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2024 + +use clap::{Args, Parser, Subcommand, ValueEnum, ValueHint}; +use log::warn; +use utils::CertificateOptions; + +/// create, perform, and verify attestation measurements +/// +/// Create, perform, and verify attestation measurements for IBM Secure Execution guest systems. +#[derive(Parser, Debug)] +pub struct CliOptions { + /// Provide more detailed output + #[arg(short='v', long, action = clap::ArgAction::Count)] + verbose: u8, + + /// Deprecated short verbose flag (-V) form the C implementation. + /// + /// If specified a deprecation warning is emitted, + #[arg(short = 'V', hide = true, action = clap::ArgAction::Count)] + verbose_deprecated: u8, + + /// Print version information and exit + #[arg(long)] + pub version: bool, + + #[command(subcommand)] + pub cmd: Command, +} + +impl CliOptions { + pub fn verbosity(&self) -> u8 { + let verbose_deprecated = self.verbose_deprecated + + match &self.cmd { + Command::Create(cmd) => cmd.verbose_deprecated, + Command::Perform(cmd) => cmd.verbose_deprecated, + Command::Verify(cmd) => cmd.verbose_deprecated, + Command::Version => 0, + }; + if verbose_deprecated > 0 { + warn!("WARNING: Use of deprecated flag '-V'. Use '-v' or '--verbose' instead.") + } + verbose_deprecated + + self.verbose + + match &self.cmd { + Command::Create(cmd) => cmd.verbose, + Command::Perform(cmd) => cmd.verbose, + Command::Verify(cmd) => cmd.verbose, + Command::Version => 0, + } + } +} + +#[derive(Subcommand, Debug)] +pub enum Command { + /// Create an attestation measurement request + /// + /// Create attestation measurement requests to attest an IBM Secure Execution guest. Only build + /// attestation requests in a trusted environment such as your Workstation. To avoid + /// compromising the attestation do not publish the attestation request protection key and + /// shred it after verification. Every 'create' will generate a new, random protection key. + Create(Box), + + /// Send the attestation request to the Ultravisor + /// + /// Run a measurement of this system through ’/dev/uv’. This device must be accessible and the + /// attestation Ultravisor facility must be present. The input must be an attestation request + /// created with ’pvattest create’. Output will contain the original request and the response + /// from the Ultravisor. + Perform(PerformAttOpt), + + /// Verify an attestation response + /// + /// Verify that a previously generated attestation measurement of an IBM Secure Execution guest + /// is as expected. Only verify attestation requests in a trusted environment, such as your + /// workstation. Input must contain the response as produced by ’pvattest perform’. The + /// protection key must be the one that was used to create the request by ’pvattest create’. + /// Shred the protection key after the verification. The header must be the IBM Secure + /// Execution header of the image that was attested during ’pvattest perform’ + Verify(VerifyOpt), + + /// Print version information and exit. + #[command(aliases(["--version"]), hide(true))] + Version, +} + +#[derive(Args, Debug)] +pub struct CreateAttOpt { + #[command(flatten)] + pub certificate_args: CertificateOptions, + + /// Write the generated request to FILE. + #[arg(short, long, value_name = "FILE", value_hint = ValueHint::FilePath,)] + pub output: String, + + /// Save the protection key as unencrypted GCM-AES256 key in FILE + /// + /// Do not publish this key, otherwise your attestation is compromised. + #[arg(long, value_name = "FILE", value_hint = ValueHint::FilePath,)] + pub arpk: String, + + /// Specify-additional data for the request. + /// + /// Additional data is provided by the Ultravisor and returned during the attestation request + /// and is covered by the attestation measurement. Can be specified multiple times. + /// Optional. + #[arg(long, value_name = "FLAGS")] + pub add_data: Vec, + + /// Provide more detailed output. + #[arg(short='v', long, action = clap::ArgAction::Count)] + verbose: u8, + + /// Deprecated short verbose flag (-V) form the C implementation. + /// + /// If specified a deprecation warning is emitted, + #[arg(short = 'V', hide = true, action = clap::ArgAction::Count)] + verbose_deprecated: u8, +} + +#[derive(Debug, ValueEnum, Clone, Copy)] +pub enum AttAddFlags { + /// Request the public host-key-hash of the key that decrypted the SE-image as additional-data + PhkhImg, + /// Request the public host-key-hash of the key that decrypted the attestation request as + /// additional-data + PhkhAtt, +} + +// all members s390x only +#[derive(Args, Debug)] +pub struct PerformAttOpt { + /// Specify the request to be sent. + #[cfg(target_arch = "s390x")] + #[arg(hide=true, short, long, value_name = "FILE", value_hint = ValueHint::FilePath,)] + pub input: Option, + + /// Specify the request to be sent. + #[cfg(target_arch = "s390x")] + #[arg(value_name = "INPUT", value_hint = ValueHint::FilePath,required_unless_present("input"), conflicts_with("input"))] + pub input_pos: Option, + + /// Write the result to FILE. + #[cfg(target_arch = "s390x")] + #[arg(hide=true, short, long, value_name = "FILE", value_hint = ValueHint::FilePath,)] + pub output: Option, + + /// Write the result to FILE. + #[arg( value_name = "OUTPUT", value_hint = ValueHint::FilePath,required_unless_present("output"), conflicts_with("output"))] + #[cfg(target_arch = "s390x")] + pub output_pos: Option, + + /// Provide up to 256 bytes of user input + /// + /// User-data is arbitrary user-defined data appended to the Attestation measurement. + /// It is verified during the Attestation measurement verification. + /// May be any arbitrary data, as long as it is less or equal to 256 bytes + #[arg(short, long, value_name = "File", value_hint = ValueHint::FilePath,)] + pub user_data: Option, + + /// Provide more detailed output. + #[arg(short='v', long, action = clap::ArgAction::Count)] + verbose: u8, + + /// Deprecated short verbose flag (-V) form the C implementation. + /// + /// If specified a deprecation warning is emitted, + #[arg(short = 'V', hide = true, action = clap::ArgAction::Count)] + verbose_deprecated: u8, +} + +#[cfg(target_arch = "s390x")] +pub struct PerformAttOptComb<'a> { + pub input: &'a str, + pub output: &'a str, + pub user_data: Option<&'a str>, +} + +#[cfg(target_arch = "s390x")] +impl<'a> From<&'a PerformAttOpt> for PerformAttOptComb<'a> { + fn from(value: &'a PerformAttOpt) -> Self { + let input = match (&value.input, &value.input_pos) { + (None, Some(i)) => i, + (Some(i), None) => i, + (Some(_), Some(_)) => unreachable!(), + (None, None) => unreachable!(), + }; + let output = match (&value.output, &value.output_pos) { + (None, Some(o)) => o, + (Some(o), None) => o, + (Some(_), Some(_)) => unreachable!(), + (None, None) => unreachable!(), + }; + let user_data = value.user_data.as_deref(); + Self { + input, + output, + user_data, + } + } +} + +#[derive(Args, Debug)] +pub struct VerifyOpt { + /// Specify the attestation request to be verified. + #[arg(short, long, value_name = "FILE", value_hint = ValueHint::FilePath,)] + pub input: String, + + /// Specify the output for the verification result + #[arg(short, long, value_name = "FILE", value_hint = ValueHint::FilePath,)] + pub output: Option, + + /// Specifies the header of the guest image. + /// + /// Can be an IBM Secure Execution image created by genprotimg or an extracted IBM Secure + /// Execution header. The header must start at a page boundary. + #[arg(long, value_name = "FILE", value_hint = ValueHint::FilePath)] + pub hdr: String, + + /// Use FILE as the protection key to decrypt the request + /// + /// Do not publish this key, otherwise your attestation is compromised. + /// Delete this key after verification. + #[arg(long, value_name = "FILE", value_hint = ValueHint::FilePath,)] + pub arpk: String, + + /// Define the output format. + #[arg(long, value_enum, default_value_t)] + pub format: VerifyOutputType, + + /// Write the user data to the FILE if any. + /// + /// Writes the user data, if the response contains any, to FILE + /// The user-data is part of the attestation measurement. If the user-data is written to FILE + /// the user-data was part of the measurement and verified. + /// Emits a warning if the response contains no user-data + #[arg(long, short ,value_name = "FILE", value_hint = ValueHint::FilePath,)] + pub user_data: Option, + + /// Provide more detailed output. + #[arg(short='v', long, action = clap::ArgAction::Count)] + verbose: u8, + + /// Deprecated short verbose flag (-V) form the C implementation. + /// + /// If specified a deprecation warning is emitted, + #[arg(short = 'V', hide = true, action = clap::ArgAction::Count)] + verbose_deprecated: u8, +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Default)] +pub enum VerifyOutputType { + /// Use yaml format. + #[default] + Yaml, +} diff --git a/rust/pvattest/src/cmd.rs b/rust/pvattest/src/cmd.rs new file mode 100644 index 00000000..a5cf7a32 --- /dev/null +++ b/rust/pvattest/src/cmd.rs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2024 +// +pub mod create; +#[cfg(target_arch = "s390x")] +pub mod perform; +pub mod verify; + +pub use create::create; +pub use verify::verify; + +pub const CMD_FN: &[&str] = &["+create", "+verify"]; +// s390 branch +#[cfg(target_arch = "s390x")] +mod uv_cmd { + pub use super::perform::perform; + pub const UV_CMD_FN: &[&str] = &["+perform"]; +} + +// non s390-branch +#[cfg(not(target_arch = "s390x"))] +mod uv_cmd { + use std::process::ExitCode; + + use anyhow::{bail, Result}; + + pub fn perform(_: &crate::cli::PerformAttOpt) -> Result { + bail!("Command only available on s390x") + } + pub const UV_CMD_FN: &[&str] = &[]; +} +pub use uv_cmd::*; diff --git a/rust/pvattest/src/cmd/create.rs b/rust/pvattest/src/cmd/create.rs new file mode 100644 index 00000000..e32007d0 --- /dev/null +++ b/rust/pvattest/src/cmd/create.rs @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2024 + +use crate::{ + cli::{AttAddFlags, CreateAttOpt}, + exchange::{ExchangeFormatRequest, ExchangeFormatVersion}, +}; +use anyhow::{bail, Context, Result}; +use log::{debug, warn}; +use pv::{ + attest::{AttestationFlags, AttestationMeasAlg, AttestationRequest, AttestationVersion}, + misc::{create_file, write_file}, + request::{ReqEncrCtx, Request, SymKey, SymKeyType}, +}; +use std::process::ExitCode; + +fn flags(cli_flags: &[AttAddFlags]) -> AttestationFlags { + let mut att_flags = AttestationFlags::default(); + for flag in cli_flags { + match flag { + AttAddFlags::PhkhImg => att_flags.set_image_phkh(), + AttAddFlags::PhkhAtt => att_flags.set_attest_phkh(), + } + } + att_flags +} + +pub fn create(opt: &CreateAttOpt) -> Result { + let att_version = AttestationVersion::One; + 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(k)); + debug!("Added all host-keys"); + + let encr_ctx = + ReqEncrCtx::random(SymKeyType::Aes256).context("Failed to generate random input")?; + let ser_arcb = arcb.encrypt(&encr_ctx)?; + warn!("Successfully generated the request"); + + let mut output = create_file(&opt.output)?; + let exch_ctx = ExchangeFormatRequest::new( + ser_arcb, + meas_alg.exp_size(), + arcb.flags().expected_additional_size(), + )?; + exch_ctx.write(&mut output, ExchangeFormatVersion::One)?; + + let arpk = match encr_ctx.prot_key() { + SymKey::Aes256(k) => k, + _ => bail!("Unexpected key type"), + }; + write_file( + &opt.arpk, + arpk.value(), + "Attestation request Protection Key", + )?; + + Ok(ExitCode::SUCCESS) +} diff --git a/rust/pvattest/src/cmd/perform.rs b/rust/pvattest/src/cmd/perform.rs new file mode 100644 index 00000000..be2b931c --- /dev/null +++ b/rust/pvattest/src/cmd/perform.rs @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2024 + +use crate::{ + cli::PerformAttOptComb, + exchange::{ExchangeFormatRequest, ExchangeFormatResponse, ExchangeFormatVersion}, +}; +use anyhow::Result; +use pv::{ + misc::{create_file, open_file, read_file}, + uv::{AttestationCmd, UvDevice}, +}; +use std::process::ExitCode; + +pub fn perform<'a, P>(opt: P) -> Result +where + P: Into>, +{ + let opt = opt.into(); + let mut input = open_file(opt.input)?; + let mut output = create_file(opt.output)?; + let uvdevice = UvDevice::open()?; + + let ex_in = ExchangeFormatRequest::read(&mut input)?; + let user_data = opt + .user_data + .map(|u| read_file(u, "user-data")) + .transpose()?; + + let mut cmd = AttestationCmd::new_request( + ex_in.arcb.clone().into(), + user_data.clone(), + ex_in.exp_measurement, + ex_in.exp_additional, + )?; + + uvdevice.send_cmd(&mut cmd)?; + + let measurement = cmd.measurement(); + let additional = cmd.additional_owned(); + let cuid = cmd.cuid(); + + let ex_out = ExchangeFormatResponse::new( + ex_in.arcb, + measurement.to_owned(), + additional, + user_data, + cuid.to_owned(), + )?; + ex_out.write(&mut output, ExchangeFormatVersion::One)?; + + Ok(ExitCode::SUCCESS) +} diff --git a/rust/pvattest/src/cmd/verify.rs b/rust/pvattest/src/cmd/verify.rs new file mode 100644 index 00000000..b1270905 --- /dev/null +++ b/rust/pvattest/src/cmd/verify.rs @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2024 + +use anyhow::Result; +use log::{debug, warn}; +use pv::{ + attest::{ + AdditionalData, AttestationFlags, AttestationItems, AttestationMeasurement, + AttestationRequest, + }, + misc::{create_file, open_file, read_exact_file, write_file}, + request::{openssl::pkey::PKey, BootHdrTags, Confidential, SymKey}, +}; +use serde::Serialize; +use std::{fmt::Display, process::ExitCode}; +use utils::HexSlice; + +use crate::{ + cli::{VerifyOpt, VerifyOutputType}, + exchange::ExchangeFormatResponse, + EXIT_CODE_ATTESTATION_FAIL, +}; + +#[derive(Serialize)] +struct VerifyOutput<'a> { + cuid: HexSlice<'a>, + #[serde(skip_serializing_if = "Option::is_none")] + add: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + add_fields: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + user_data: Option>, +} + +impl<'a> VerifyOutput<'a> { + fn from_exchange(resp: &'a ExchangeFormatResponse, flags: &AttestationFlags) -> Result { + let additional_data_fields = resp + .additional() + .map(|a| AdditionalData::from_slice(a, flags)) + .transpose()?; + let user_data = resp.user().map(|u| u.into()); + + Ok(Self { + cuid: resp.config_uid().into(), + add: resp.additional().map(|a| a.into()), + add_fields: additional_data_fields.map(AdditionalData::from_other), + user_data, + }) + } +} + +impl<'a> Display for VerifyOutput<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Config UID:")?; + writeln!(f, "{:#}", self.cuid)?; + if let Some(data) = &self.add { + writeln!(f, "Additional-data:")?; + writeln!(f, "{:#}", data)?; + } + if let Some(data) = &self.add_fields { + writeln!(f, "Additional-data content:")?; + writeln!(f, "{:#}", data)?; + } + if let Some(data) = &self.user_data { + writeln!(f, "user-data:")?; + writeln!(f, "{:#}", data)?; + } + Ok(()) + } +} + +pub fn verify(opt: &VerifyOpt) -> Result { + let mut input = open_file(&opt.input)?; + let mut img = open_file(&opt.hdr)?; + let output = opt.output.as_ref().map(create_file).transpose()?; + let arpk = SymKey::Aes256( + read_exact_file(&opt.arpk, "Attestation request protection key").map(Confidential::new)?, + ); + let tags = BootHdrTags::from_se_image(&mut img)?; + let exchange = ExchangeFormatResponse::read(&mut input)?; + + let (auth, conf) = AttestationRequest::decrypt_bin(exchange.arcb(), &arpk)?; + let meas_key = PKey::hmac(conf.measurement_key())?; + let items = AttestationItems::new( + &tags, + exchange.config_uid(), + exchange.user(), + conf.nonce().as_ref().map(|v| v.value()), + exchange.additional(), + ); + + let measurement = AttestationMeasurement::calculate(items, auth.mai(), &meas_key)?; + + let uv_meas = exchange.measurement(); + if !measurement.eq_secure(uv_meas) { + debug!("Measurement values:"); + debug!("Recieved: {}", HexSlice::from(uv_meas)); + debug!("Calculated: {}", HexSlice::from(measurement.as_ref())); + warn!("Attestation measurement verification failed. Calculated and received attestation measurement are not equal."); + return Ok(ExitCode::from(EXIT_CODE_ATTESTATION_FAIL)); + } + warn!("Attestation measurement verified"); + // Error impossible CUID is present Attestation verified + let pr_data = VerifyOutput::from_exchange(&exchange, auth.flags())?; + + warn!("{pr_data}"); + if let Some(mut output) = output { + match opt.format { + VerifyOutputType::Yaml => serde_yaml::to_writer(&mut output, &pr_data)?, + }; + } + + if let Some(user_data) = &opt.user_data { + match exchange.user() { + Some(data) => write_file(user_data, data, "user-data")?, + None => { + warn!("Location for `user-data` specified, but respose does not contain any user-data") + } + } + }; + + Ok(ExitCode::SUCCESS) +} diff --git a/rust/pvattest/src/exchange.rs b/rust/pvattest/src/exchange.rs new file mode 100644 index 00000000..a55bbe51 --- /dev/null +++ b/rust/pvattest/src/exchange.rs @@ -0,0 +1,884 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2024 +use anyhow::{anyhow, bail, Error, Result}; +use byteorder::ByteOrder; +use pv::{assert_size, request::MagicValue, uv::AttestationCmd, uv::ConfigUid}; +use std::{ + io::{ErrorKind, Read, Seek, SeekFrom, Write}, + mem::size_of, +}; +use zerocopy::{AsBytes, BigEndian, FromBytes, FromZeroes, U32, U64}; + +const INV_EXCHANGE_FMT_ERROR_TEXT: &str = "The input has not the correct format:"; + +#[repr(C)] +#[derive(Debug, AsBytes, PartialEq, Eq, Default, FromZeroes, FromBytes)] +struct Entry { + size: U32, + offset: U32, +} +assert_size!(Entry, 8); + +/// If size == 0 the offset is ignored. (entry does not exist) +/// If offset >0 and <0x40 -> invalid format +/// If offset == 0 and size > 0 no data saved, however the request will need this amount of memory +/// to succeed. Only makes sense for measurement and additional data. This however, is not +/// enforced. +impl Entry { + fn new(size: u32, offset: u32) -> Self { + Self { + size: size.into(), + offset: offset.into(), + } + } + + /// # Panic + /// + /// panics if `val` is larger than `max_size` bytes + fn from_slice(val: Option<&[u8]>, max_size: u32, offset: &mut u32) -> Self { + match val { + Some(val) => { + assert!(val.len() <= max_size as usize); + let size = val.len() as u32; + let res = Self::new(size, *offset); + *offset += size; + res + } + None => Self::default(), + } + } + + /// # Panic + /// + /// panics if `val` is larger than `max_size` bytes + fn from_exp(val: Option) -> Self { + if let Some(val) = val { + Self::new(val, 0) + } else { + Self::default() + } + } + + fn from_none() -> Self { + Self::default() + } + + /// Reads data from stream if required + fn read(&self, reader: &mut R) -> Result + where + R: Read + Seek, + { + match self { + Entry { size, .. } if size.get() == 0 => Ok(ExpOrData::None), + Entry { size, offset } if offset.get() == 0 => Ok(ExpOrData::Exp(size.get())), + Entry { size, offset } => { + reader.seek(SeekFrom::Start(offset.get() as u64))?; + let mut buf = vec![0; size.get() as usize]; + reader.read_exact(&mut buf)?; + Ok(ExpOrData::Data(buf)) + } + } + } +} + +#[repr(C)] +#[derive(Debug, AsBytes, FromZeroes, FromBytes)] +struct ExchangeFormatV1Hdr { + magic: U64, + version: U32, + size: U32, + reserved: U64, + /// v1 specific + arcb: Entry, + measurement: Entry, + additional: Entry, + user: Entry, + config_uid: Entry, +} +assert_size!(ExchangeFormatV1Hdr, 0x40); + +impl ExchangeFormatV1Hdr { + fn new_request(arcb: &[u8], measurement: u32, additional: u32) -> Result { + let mut offset: u32 = size_of::() as u32; + let arcb_entry = Entry::from_slice(Some(arcb), AttestationCmd::ARCB_MAX_SIZE, &mut offset); + let measurement_entry = Entry::from_exp(Some(measurement)); + let exp_add = match additional { + 0 => None, + size => Some(size), + }; + // TODO min and max size check? + let additional_entry = Entry::from_exp(exp_add); //, AttestationCmd::ADDITIONAL_MAX_SIZE, &mut offset); + let user_entry = Entry::from_none(); + let cuid_entry = Entry::from_none(); + + Ok(Self { + magic: U64::from_bytes(ExchangeMagic::MAGIC), + version: ExchangeFormatVersion::One.into(), + size: offset.into(), + reserved: 0.into(), + arcb: arcb_entry, + measurement: measurement_entry, + additional: additional_entry, + user: user_entry, + config_uid: cuid_entry, + }) + } + + fn new_response( + arcb: &[u8], + measurement: &[u8], + additional: Option<&[u8]>, + user: Option<&[u8]>, + config_uid: &[u8], + ) -> Result { + let mut offset: u32 = size_of::() as u32; + let arcb_entry = Entry::from_slice(Some(arcb), AttestationCmd::ARCB_MAX_SIZE, &mut offset); + let measurement_entry = Entry::from_slice( + Some(measurement), + AttestationCmd::MEASUREMENT_MAX_SIZE, + &mut offset, + ); + let additional_entry = + Entry::from_slice(additional, AttestationCmd::ADDITIONAL_MAX_SIZE, &mut offset); + let user_entry = Entry::from_slice(user, AttestationCmd::USER_MAX_SIZE, &mut offset); + let cuid_entry = Entry::from_slice(Some(config_uid), 0x10, &mut offset); + + Ok(Self { + magic: U64::from_bytes(ExchangeMagic::MAGIC), + version: ExchangeFormatVersion::One.into(), + size: offset.into(), + reserved: 0.into(), + arcb: arcb_entry, + measurement: measurement_entry, + additional: additional_entry, + user: user_entry, + config_uid: cuid_entry, + }) + } +} + +/// The magic value used to identify an [`ExchangeFormatRequest`] +/// +/// The magic value is ASCII: +/// ```rust +/// # use s390_pv_core::attest::ExchangeMagic; +/// # use s390_pv_core::request::MagicValue; +/// # fn main() { +/// # let magic = +/// b"pvattest" +/// # ; +/// # assert!(ExchangeMagic::starts_with_magic(magic)); +/// # } +/// ``` +pub struct ExchangeMagic; +impl MagicValue<8> for ExchangeMagic { + const MAGIC: [u8; 8] = [0x70, 0x76, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74]; +} + +/// Version identifier for an [`ExchangeFormatRequest`] +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExchangeFormatVersion { + /// Version 1 (= 0x0100) + One = 0x0100, +} + +impl TryFrom> for ExchangeFormatVersion { + type Error = Error; + + fn try_from(value: U32) -> Result { + if value.get() == Self::One as u32 { + Ok(Self::One) + } else { + bail!( + "{INV_EXCHANGE_FMT_ERROR_TEXT} Unsupported version: ({})", + value.get() + ); + } + } +} + +impl From for U32 { + fn from(value: ExchangeFormatVersion) -> Self { + (value as u32).into() + } +} + +/// A parsed exchange entry value +/// +/// An entry can be all zero(None), just a size (Exp) or a offset+size to some data (Data) +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum ExpOrData { + Exp(u32), + Data(Vec), + None, +} + +impl ExpOrData { + /// calculates the (expected or real) size + fn size(&self) -> u32 { + match self { + ExpOrData::Exp(s) => *s, + // size is max u32 large as read in before + ExpOrData::Data(v) => v.len() as u32, + ExpOrData::None => 0, + } + } + + /// Returns data if self is [`ExpOrData::Data`] + /// + /// Consumes itself + fn data(self) -> Option> { + match self { + ExpOrData::Data(v) => Some(v), + _ => None, + } + } +} + +impl From> for ExpOrData { + fn from(value: Option) -> Self { + match value { + Some(v) => ExpOrData::Exp(v), + None => ExpOrData::None, + } + } +} + +impl From<&ExpOrData> for Option { + fn from(value: &ExpOrData) -> Self { + match value { + ExpOrData::Exp(v) => Some(*v), + _ => None, + } + } +} + +impl From for Option> { + fn from(value: ExpOrData) -> Self { + match value { + ExpOrData::Exp(s) => Some(vec![0; s as usize]), + ExpOrData::Data(d) => Some(d), + ExpOrData::None => None, + } + } +} + +/// The _exchange format_ is a simple file format to send labeled binary blobs between +/// pvattest instances on different machines. +#[derive(Debug, PartialEq, Eq)] +pub struct ExchangeFormatRequest { + // all sizes are guaranteed to fit in the exchange format/UV Call at any time + // pub to allow deconstruction of this struct + pub arcb: Vec, + pub exp_measurement: u32, + pub exp_additional: u32, +} + +/// The _exchange format_ is a simple file format to send labeled binary blobs between +/// pvattest instances on different machines. +#[derive(Debug, PartialEq, Eq)] +pub struct ExchangeFormatResponse { + // all sizes are guaranteed to fit in the exchange format/UV Call at any time + // pub to allow deconstruction of this struct + pub arcb: Vec, + pub measurement: Vec, + pub additional: Option>, + pub user: Option>, + pub config_uid: ConfigUid, +} + +impl ExchangeFormatRequest { + /// Creates a new exchange context, with an attestation request, expected measurement and + /// optional an additional data size. Useful for creating a attestation request. + pub fn new(arcb: Vec, exp_measurement: u32, exp_additional: u32) -> Result { + verify_size( + exp_measurement, + 1, + AttestationCmd::MEASUREMENT_MAX_SIZE, + "Expected measurement size", + )?; + verify_size( + exp_additional, + 0, + AttestationCmd::ADDITIONAL_MAX_SIZE, + "Expected additional data size", + )?; + verify_slice(&arcb, AttestationCmd::ARCB_MAX_SIZE, "Attestation request")?; + + Ok(Self { + arcb, + exp_measurement, + exp_additional, + }) + } + + fn write_v1(&self, writer: &mut W) -> Result<()> + where + W: Write, + { + let hdr = ExchangeFormatV1Hdr::new_request( + self.arcb.as_slice(), + self.exp_measurement, + self.exp_additional, + )?; + writer.write_all(hdr.as_bytes())?; + writer.write_all(&self.arcb)?; + Ok(()) + } + + /// Serializes the encapsulated data into the provides stream in the provided format + pub fn write(&self, writer: &mut W, version: ExchangeFormatVersion) -> Result<()> + where + W: Write, + { + match version { + ExchangeFormatVersion::One => self.write_v1(writer), + } + } + + /// Reads and deserializes the exchange file in the provided stream + /// + /// # Errors + /// + /// Returns an error if the stream does not contain data in exchange format, CUID or user data + /// do not fit, or any IO error that can appear during reading streams. + pub fn read(reader: &mut R) -> Result + where + R: Read + Seek, + { + let mut buf = vec![0; size_of::()]; + match reader.read_exact(&mut buf) { + Ok(it) => it, + // report hdr file to small for header + Err(err) if err.kind() == ErrorKind::UnexpectedEof => { + bail!("{INV_EXCHANGE_FMT_ERROR_TEXT} Invalid Header."); + } + Err(err) => return Err(err.into()), + }; + + if !ExchangeMagic::starts_with_magic(&buf) { + bail!("{INV_EXCHANGE_FMT_ERROR_TEXT} Does not start with the magic value.",); + } + + let hdr = ExchangeFormatV1Hdr::ref_from(buf.as_slice()) + .ok_or(anyhow!("{INV_EXCHANGE_FMT_ERROR_TEXT} Invalid Header."))?; + + match TryInto::::try_into(hdr.version)? { + ExchangeFormatVersion::One => (), + } + + if stream_len(reader)? < hdr.size.get() as u64 { + bail!("{INV_EXCHANGE_FMT_ERROR_TEXT} File size too small"); + } + let arcb = hdr.arcb.read(reader)?.data().ok_or(anyhow!( + "{INV_EXCHANGE_FMT_ERROR_TEXT} Contains no attestation request.", + ))?; + + let measurement = hdr.measurement.read(reader)?.size(); + let additional = hdr.additional.read(reader)?.size(); + Self::new(arcb, measurement, additional) + } +} + +// Seek::stream_is unstable +// not expose to API users +// taken from rust std::io::seek; +fn stream_len(seek: &mut S) -> Result +where + S: Seek, +{ + let old_pos = seek.stream_position()?; + let len = seek.seek(SeekFrom::End(0))?; + + // Avoid seeking a third time when we were already at the end of the + // stream. The branch is usually way cheaper than a seek operation. + if old_pos != len { + seek.seek(SeekFrom::Start(old_pos))?; + } + + Ok(len) +} + +fn verify_size(size: u32, min_size: u32, max_size: u32, field: &'static str) -> Result<()> { + if size < min_size { + bail!("{INV_EXCHANGE_FMT_ERROR_TEXT} The {field} field is too small ({size})"); + } + + if size > max_size { + bail!("{INV_EXCHANGE_FMT_ERROR_TEXT} The {field} field is too large ({size})"); + } + + Ok(()) +} + +/// check that a slice has at max `max_size` amount of bytes +fn verify_slice(val: &[u8], max_size: u32, field: &'static str) -> Result<()> { + if val.len() > max_size as usize { + bail!( + "{INV_EXCHANGE_FMT_ERROR_TEXT} The {field} field is too large ({})", + val.len() + ); + } + Ok(()) +} + +impl ExchangeFormatResponse { + /// Creates a new exchange context, with an attestation request, measurement and + /// cuid. + pub fn new( + arcb: Vec, + measurement: Vec, + additional: Option>, + user: Option>, + config_uid: ConfigUid, + ) -> Result { + // should not fail; Already checked during import. + verify_slice( + &arcb, + AttestationCmd::ARCB_MAX_SIZE, + "Attestation request data", + )?; + verify_slice( + &measurement, + AttestationCmd::MEASUREMENT_MAX_SIZE, + "Attestation Measurement", + )?; + + if let Some(additional) = &additional { + verify_slice( + additional, + AttestationCmd::ADDITIONAL_MAX_SIZE, + "Additional data", + )?; + } + + if let Some(user) = &user { + verify_slice(user, AttestationCmd::USER_MAX_SIZE, "User data")?; + } + + Ok(Self { + arcb, + measurement, + additional, + user, + config_uid, + }) + } + + fn write_v1(&self, writer: &mut W) -> Result<()> + where + W: Write, + { + let hdr = ExchangeFormatV1Hdr::new_response( + self.arcb.as_slice(), + &self.measurement, + self.additional.as_deref(), + self.user.as_deref(), + &self.config_uid, + )?; + writer.write_all(hdr.as_bytes())?; + writer.write_all(&self.arcb)?; + writer.write_all(&self.measurement)?; + if let Some(data) = &self.additional { + writer.write_all(data)?; + } + if let Some(data) = &self.user { + writer.write_all(data)?; + } + writer.write_all(&self.config_uid)?; + Ok(()) + } + + /// Serializes the encapsulated data into the provides stream in the provided format + pub fn write(&self, writer: &mut W, version: ExchangeFormatVersion) -> Result<()> + where + W: Write, + { + match version { + ExchangeFormatVersion::One => self.write_v1(writer), + } + } + + /// Reads and deserializes the exchange file in the provided stream + /// + /// # Errors + /// + /// Returns an error if the stream does not contain data in exchange format, CUID or user data + /// do not fit, or any IO error that can appear during reading streams. + pub fn read(reader: &mut R) -> Result + where + R: Read + Seek, + { + let mut buf = vec![0; size_of::()]; + match reader.read_exact(&mut buf) { + Ok(it) => it, + // report hdr file to small for header + Err(err) if err.kind() == ErrorKind::UnexpectedEof => { + bail!("{INV_EXCHANGE_FMT_ERROR_TEXT} Invalid Header."); + } + Err(err) => return Err(err.into()), + }; + + if !ExchangeMagic::starts_with_magic(&buf) { + bail!("{INV_EXCHANGE_FMT_ERROR_TEXT} Does not start with the magic value."); + } + + let hdr = ExchangeFormatV1Hdr::ref_from(buf.as_slice()) + .ok_or(anyhow!("{INV_EXCHANGE_FMT_ERROR_TEXT} Invalid Header."))?; + + match TryInto::::try_into(hdr.version)? { + ExchangeFormatVersion::One => (), + } + + if stream_len(reader)? < hdr.size.get() as u64 { + bail!("{INV_EXCHANGE_FMT_ERROR_TEXT} File size too small"); + } + let arcb = hdr.arcb.read(reader)?.data().ok_or(anyhow!( + "{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).", + ))?; + let additional = hdr.additional.read(reader)?.data(); + let user = hdr.user.read(reader)?.data(); + let config_uid: ConfigUid = match hdr.config_uid.read(reader)?.data() { + Some(v) => v.try_into().map_err(|_| { +anyhow!( + "{INV_EXCHANGE_FMT_ERROR_TEXT} Configuration UID has an invalid size. Expected size 16, is {}",hdr.config_uid.size.get() + ) + })?, + None => + bail!("{INV_EXCHANGE_FMT_ERROR_TEXT} Contains no attestation response (CUID missing).") +, + }; + Self::new(arcb, measurement, additional, user, config_uid) + } + + /// Returns the measurement of this [`ExchangeFormatRequest`]. + pub fn measurement(&self) -> &[u8] { + &self.measurement + } + + /// Returns the additional data of this [`ExchangeFormatRequest`]. + pub fn additional(&self) -> Option<&[u8]> { + self.additional.as_deref() + } + + /// Returns the user data of this [`ExchangeFormatRequest`]. + pub fn user(&self) -> Option<&[u8]> { + self.user.as_deref() + } + + /// Returns the config UID of this [`ExchangeFormatRequest`]. + /// + /// # Error + /// Returns an error if the [`ExchangeFormatRequest`] contains no CUID, + pub fn config_uid(&self) -> &ConfigUid { + &self.config_uid + } + + /// Returns a reference to the attestation request of this [`ExchangeFormatRequest`]. + pub fn arcb(&self) -> &[u8] { + self.arcb.as_ref() + } +} + +#[cfg(test)] +mod test { + + use std::io::Cursor; + + use super::*; + use pv::misc::read_file; + + #[test] + fn exchange_from_slice() { + let val = &[0; 17]; + let mut offset = 18; + + let entry = Entry::from_slice(Some(val), 20, &mut offset); + assert_eq!( + entry, + Entry { + size: 17.into(), + offset: 18.into(), + } + ); + assert_eq!(offset, 18 + 17); + } + static ARCB: [u8; 16] = [0x11; 16]; + static MEASUREMENT: [u8; 64] = [0x12; 64]; + static ADDITIONAL: [u8; 32] = [0x13; 32]; + static CUID: [u8; 16] = [0x14; 16]; + static USER: [u8; 256] = [0x15; 256]; + + fn test_read_write_request( + path: &'static str, + arcb: Vec, + 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 mut out = create_file(path).unwrap(); + let mut out = vec![]; + ctx_write + .write(&mut out, ExchangeFormatVersion::One) + .unwrap(); + + let buf = read_file(path, "test read exchange").unwrap(); + + assert_eq!(out, buf); + + let ctx_read = ExchangeFormatRequest::read(&mut Cursor::new(&mut &buf)).unwrap(); + + assert_eq!(ctx_read, ctx_write); + } + + fn test_read_write_response( + path: &'static str, + arcb: Vec, + measurement: Vec, + additional: Option>, + user: Option>, + cuid: ConfigUid, + ) { + let ctx_write = ExchangeFormatResponse::new(arcb, measurement, additional, user, cuid) + .expect("exchange fmt creation"); + + // let mut out = create_file(path).unwrap(); + + let mut out = vec![]; + ctx_write + .write(&mut out, ExchangeFormatVersion::One) + .unwrap(); + + let buf = read_file(path, "test read exchange").unwrap(); + + assert_eq!(out, buf); + + let ctx_read = ExchangeFormatResponse::read(&mut Cursor::new(&mut &buf)).unwrap(); + + assert_eq!(ctx_read, ctx_write); + } + + #[test] + fn full_req() { + test_read_write_request( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/assets/", + "exp/exchange/full_req.bin" + ), + ARCB.to_vec(), + MEASUREMENT.len(), + ADDITIONAL.len(), + ); + } + + #[test] + fn add_req() { + test_read_write_request( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/assets/", + "exp/exchange/add_req.bin" + ), + ARCB.to_vec(), + MEASUREMENT.len(), + ADDITIONAL.len(), + ); + } + + #[test] + fn invalid_req() { + ExchangeFormatRequest::new(ARCB.to_vec(), 0, ADDITIONAL.len() as u32).unwrap_err(); + } + + #[test] + fn min_req() { + test_read_write_request( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/assets/", + "exp/exchange/min_req.bin" + ), + ARCB.to_vec(), + MEASUREMENT.len(), + 0, + ); + } + + #[test] + fn full_resp() { + test_read_write_response( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/assets/", + "exp/exchange/full_resp.bin" + ), + ARCB.to_vec(), + MEASUREMENT.to_vec(), + ADDITIONAL.to_vec().into(), + USER.to_vec().into(), + CUID, + ); + } + + #[test] + fn add_resp() { + test_read_write_response( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/assets/", + "exp/exchange/add_resp.bin" + ), + ARCB.to_vec(), + MEASUREMENT.to_vec(), + ADDITIONAL.to_vec().into(), + None, + CUID, + ); + } + + #[test] + fn user_resp() { + test_read_write_response( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/assets/", + "exp/exchange/user_resp.bin" + ), + ARCB.to_vec(), + MEASUREMENT.to_vec(), + None, + USER.to_vec().into(), + CUID, + ); + } + #[test] + fn min_resp() { + test_read_write_response( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/assets/", + "exp/exchange/min_resp.bin" + ), + ARCB.to_vec(), + MEASUREMENT.to_vec(), + None, + None, + CUID, + ) + } + + #[test] + fn resp_no_cuid() { + let buf = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/assets/", + "exp/exchange/min_req.bin" + )); + let _ctx_read = ExchangeFormatResponse::read(&mut Cursor::new(&mut &buf)).unwrap_err(); + } + + #[test] + fn resp_inv_magic() { + let mut buf = read_file( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/assets/", + "exp/exchange/min_req.bin" + ), + "test resp inv magic", + ) + .unwrap(); + // tamper with the magic + buf[0] = !buf[0]; + + let _ctx_read = ExchangeFormatResponse::read(&mut Cursor::new(&mut &buf)).unwrap_err(); + } + + #[test] + fn no_arcb() { + let mut buf = read_file( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/assets/", + "exp/exchange/min_req.bin" + ), + "test resp inv magic", + ) + .unwrap(); + // delete the arcb entry + buf[0x18..0x20].copy_from_slice(&[0; 8]); + + let _ctx_read = ExchangeFormatRequest::read(&mut Cursor::new(&mut &buf)).unwrap_err(); + } + + #[test] + fn small() { + let mut buf = read_file( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/assets/", + "exp/exchange/min_req.bin" + ), + "test resp inv magic", + ) + .unwrap(); + buf.pop(); + + let _ctx_read = ExchangeFormatRequest::read(&mut Cursor::new(&mut &buf)).unwrap_err(); + } + + #[test] + fn hdr() { + // buffer smaller than the header but containing the magic + let buf = [ + 0x70, 0x76, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x1, 0x2, 0x3, 0x4, + ]; + + let _ctx_read = ExchangeFormatRequest::read(&mut Cursor::new(&mut &buf)).unwrap_err(); + } + + #[test] + fn version() { + let mut buf = read_file( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/assets/", + "exp/exchange/min_req.bin" + ), + "test resp inv magic", + ) + .unwrap(); + // tamper with the version + buf[0x8] = 0xff; + + let _ctx_read = ExchangeFormatRequest::read(&mut Cursor::new(&mut &buf)).unwrap_err(); + } + + #[test] + fn cuid_size() { + let mut buf = read_file( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/assets/", + "exp/exchange/min_resp.bin" + ), + "test resp inv magic", + ) + .unwrap(); + // tamper with the cuid size + buf[0x3b] = 0xf; + + let _ctx_read = ExchangeFormatResponse::read(&mut Cursor::new(&mut &buf)).unwrap_err(); + } +} diff --git a/rust/pvattest/src/main.rs b/rust/pvattest/src/main.rs new file mode 100644 index 00000000..435ddd12 --- /dev/null +++ b/rust/pvattest/src/main.rs @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2024 + +mod cli; +mod cmd; +mod exchange; + +use clap::{CommandFactory, Parser}; +use cli::CliOptions; +use log::trace; +use std::process::ExitCode; +use utils::{print_cli_error, print_error, print_version, PvLogger}; + +use crate::cli::Command; +use crate::cmd::*; + +static LOGGER: PvLogger = PvLogger; +const FEATURES: &[&[&str]] = &[cmd::CMD_FN, cmd::UV_CMD_FN]; +const EXIT_CODE_ATTESTATION_FAIL: u8 = 2; +const EXIT_CODE_LOGGER_FAIL: u8 = 3; + +fn print_version(verbosity: u8) -> anyhow::Result { + print_version!(verbosity, "2024", FEATURES.concat()); + Ok(ExitCode::SUCCESS) +} + +fn main() -> ExitCode { + let cli: CliOptions = match CliOptions::try_parse() { + Ok(cli) => cli, + Err(e) => return print_cli_error(e, CliOptions::command()), + }; + + // set up logger/stderr + if let Err(e) = LOGGER.start(cli.verbosity()) { + // should(TM) never happen + eprintln!("Logger error: {e:?}"); + return EXIT_CODE_LOGGER_FAIL.into(); + } + + trace!("Trace verbosity, may leak secrets to command-line"); + trace!("Options {cli:?}"); + + let res = match &cli.cmd { + Command::Create(opt) => create(opt), + Command::Perform(opt) => perform(opt), + Command::Verify(opt) => verify(opt), + Command::Version => print_version(cli.verbosity()), + }; + match res { + Ok(c) => c, + Err(e) => print_error(&e, cli.verbosity()), + } +} diff --git a/rust/pvattest/tests/assets/exp/exchange/add_req.bin b/rust/pvattest/tests/assets/exp/exchange/add_req.bin new file mode 100644 index 0000000000000000000000000000000000000000..e1317387ef0b8d99a94ccd23e7eafda1ae82471f GIT binary patch literal 80 mcmXRYODri#EiPeTU}OM-01yEM0zldUia|mOAOal-q5%L$q6Lir literal 0 HcmV?d00001 diff --git a/rust/pvattest/tests/assets/exp/exchange/add_resp.bin b/rust/pvattest/tests/assets/exp/exchange/add_resp.bin new file mode 100644 index 0000000000000000000000000000000000000000..96f69dbc03e25ba7a59977dadb1e1f29006a7cc7 GIT binary patch literal 192 zcmXRYODri#EiPeTU}OM-10Vtl1c0;y6bArl1t6XP#4rGo-ynzvgeU~U1b_${005_b B54`{Y literal 0 HcmV?d00001 diff --git a/rust/pvattest/tests/assets/exp/exchange/full.bin b/rust/pvattest/tests/assets/exp/exchange/full.bin new file mode 100644 index 0000000000000000000000000000000000000000..56df90dfbdcb760cdf44ee255434281be277f1d2 GIT binary patch literal 448 zcmXRYODri#EiPeTU}ON|13&@>1b_?&C=LM93P3yoqK;t$kPVb)+#rYsgeU~U1c2xW I2O?+y027lXZ~y=R literal 0 HcmV?d00001 diff --git a/rust/pvattest/tests/assets/exp/exchange/full_req.bin b/rust/pvattest/tests/assets/exp/exchange/full_req.bin new file mode 100644 index 0000000000000000000000000000000000000000..e1317387ef0b8d99a94ccd23e7eafda1ae82471f GIT binary patch literal 80 mcmXRYODri#EiPeTU}OM-01yEM0zldUia|mOAOal-q5%L$q6Lir literal 0 HcmV?d00001 diff --git a/rust/pvattest/tests/assets/exp/exchange/full_resp.bin b/rust/pvattest/tests/assets/exp/exchange/full_resp.bin new file mode 100644 index 0000000000000000000000000000000000000000..56df90dfbdcb760cdf44ee255434281be277f1d2 GIT binary patch literal 448 zcmXRYODri#EiPeTU}ON|13&@>1b_?&C=LM93P3yoqK;t$kPVb)+#rYsgeU~U1c2xW I2O?+y027lXZ~y=R literal 0 HcmV?d00001 diff --git a/rust/pvattest/tests/assets/exp/exchange/min_req.bin b/rust/pvattest/tests/assets/exp/exchange/min_req.bin new file mode 100644 index 0000000000000000000000000000000000000000..b72eccca12c257ab8403b3ced3e148b2cf1a7cf3 GIT binary patch literal 80 kcmXRYODri#EiPeTU}OM-01yEM0zldUilIW7lpq=a07C)=Z2$lO literal 0 HcmV?d00001 diff --git a/rust/pvattest/tests/assets/exp/exchange/min_resp.bin b/rust/pvattest/tests/assets/exp/exchange/min_resp.bin new file mode 100644 index 0000000000000000000000000000000000000000..a25b6a569536e718847570bab72dd6f133aebd99 GIT binary patch literal 160 tcmXRYODri#EiPeTU}OM-1t0