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 <huoqif@cn.ibm.com>
Acked-by: Marc Hartmayer <mhartmay@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
This commit is contained in:
Steffen Eiden
2024-04-05 13:28:04 +02:00
parent 4d57ff046d
commit 16610a211f
24 changed files with 2208 additions and 0 deletions

15
rust/Cargo.lock generated
View File

@@ -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"

View File

@@ -3,6 +3,7 @@ members = [
"pv",
"pv_core",
"pvapconfig",
"pvattest",
"pvsecret",
"utils",
]

17
rust/pvattest/Cargo.toml Normal file
View File

@@ -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" }

261
rust/pvattest/README.md Normal file
View File

@@ -0,0 +1,261 @@
<!--
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.
-->
# pvattest
## Synopsis
`pvattest [OPTIONS] <COMMAND>`
## Description
create, perform, and verify attestation measurements Create, perform, and verify
attestation measurements for IBM Secure Execution guest systems.
## Commands Overview
- **create**
<ul>
Create an attestation measurement request
</ul>
- **perform**
<ul>
Send the attestation request to the Ultravisor
</ul>
- **verify**
<ul>
Verify an attestation response
</ul>
- **version**
<ul>
Print version information and exit
</ul>
## Options
`-v`, `--verbose`
<ul>
Provide more detailed output
</ul>
`--version`
<ul>
Print version information and exit
</ul>
`-h`, `--help`
<ul>
Print help (see a summary with '-h')
</ul>
## pvattest create
### Synopsis
`pvattest create [OPTIONS] --host-key-document <FILE> --output <FILE> --arpk <FILE> <--no-verify|--cert <FILE>>`
### 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 <FILE>`
<ul>
Use FILE as a host-key document. Can be specified multiple times and must be
used at least once.
</ul>
`--no-verify`
<ul>
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.
</ul>
`-C`, `--cert <FILE>`
<ul>
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).
</ul>
`--crl <FILE>`
<ul>
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.
</ul>
`--offline`
<ul>
Make no attempt to download CRLs
</ul>
`--root-ca <ROOT_CA>`
<ul>
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.
</ul>
`-o`, `--output <FILE>`
<ul>
Write the generated request to FILE
</ul>
`--arpk <FILE>`
<ul>
Save the protection key as unencrypted GCM-AES256 key in FILE Do not publish
this key, otherwise your attestation is compromised.
</ul>
`--add-data <FLAGS>`
<ul>
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
</ul>
`-v`, `--verbose`
<ul>
Provide more detailed output
</ul>
`-h`, `--help`
<ul>
Print help (see a summary with '-h')
</ul>
## 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
`<INPUT>`
<ul>
Specify the request to be sent
</ul>
`<OUTPUT>`
<ul>
Write the result to FILE
</ul>
### Options
`-u`, `--user-data <File>`
<ul>
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
</ul>
`-v`, `--verbose`
<ul>
Provide more detailed output
</ul>
`-h`, `--help`
<ul>
Print help (see a summary with '-h')
</ul>
## pvattest verify
### Synopsis
`pvattest verify [OPTIONS] --input <FILE> --hdr <FILE> --arpk <FILE>`
### 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 <FILE>`
<ul>
Specify the attestation request to be verified
</ul>
`-o`, `--output <FILE>`
<ul>
Specify the output for the verification result
</ul>
`--hdr <FILE>`
<ul>
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.
</ul>
`--arpk <FILE>`
<ul>
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.
</ul>
`--format <FORMAT>`
<ul>
Define the output format
Default value: 'yaml'
Possible values:
- **yaml**: Use yaml format
</ul>
`-u`, `--user-data <FILE>`
<ul>
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
</ul>
`-v`, `--verbose`
<ul>
Provide more detailed output
</ul>
`-h`, `--help`
<ul>
Print help (see a summary with '-h')
</ul>

View File

@@ -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 <FILE> --output <FILE> --arpk <FILE> <--no-verify|--cert <FILE>>
.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 <FILE>
.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 <FILE>
.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 <FILE>
.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 <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 <FILE>
.RS 4
Write the generated request to FILE.
.RE
.RE
.PP
\-\-arpk <FILE>
.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 <FLAGS>
.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)

View File

@@ -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
<INPUT>
.RS 4
Specify the request to be sent.
.RE
.RE
.PP
<OUTPUT>
.RS 4
Write the result to FILE.
.RE
.RE
.PP
\-u, \-\-user-data <File>
.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)

View File

@@ -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 <FILE> --hdr <FILE> --arpk <FILE>
.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 <FILE>
.RS 4
Specify the attestation request to be verified.
.RE
.RE
.PP
\-o, \-\-output <FILE>
.RS 4
Specify the output for the verification result.
.RE
.RE
.PP
\-\-hdr <FILE>
.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 <FILE>
.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 <FORMAT>
.RS 4
Define the output format.
[default: 'yaml']
Possible values:
.RS 4
- \fByaml\fP: Use yaml format.
.RE
.RE
.PP
\-u, \-\-user-data <FILE>
.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)

View File

@@ -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] <COMMAND>
.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-guests 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)

257
rust/pvattest/src/cli.rs Normal file
View File

@@ -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<CreateAttOpt>),
/// 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<AttAddFlags>,
/// 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<String>,
/// 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<String>,
/// Write the result to FILE.
#[cfg(target_arch = "s390x")]
#[arg(hide=true, short, long, value_name = "FILE", value_hint = ValueHint::FilePath,)]
pub output: Option<String>,
/// 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<String>,
/// 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<String>,
/// 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<String>,
/// 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<String>,
/// 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,
}

33
rust/pvattest/src/cmd.rs Normal file
View File

@@ -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<ExitCode> {
bail!("Command only available on s390x")
}
pub const UV_CMD_FN: &[&str] = &[];
}
pub use uv_cmd::*;

View File

@@ -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<ExitCode> {
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)
}

View File

@@ -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<ExitCode>
where
P: Into<PerformAttOptComb<'a>>,
{
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)
}

View File

@@ -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<HexSlice<'a>>,
#[serde(skip_serializing_if = "Option::is_none")]
add_fields: Option<AdditionalData<HexSlice<'a>>>,
#[serde(skip_serializing_if = "Option::is_none")]
user_data: Option<HexSlice<'a>>,
}
impl<'a> VerifyOutput<'a> {
fn from_exchange(resp: &'a ExchangeFormatResponse, flags: &AttestationFlags) -> Result<Self> {
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<ExitCode> {
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)
}

View File

@@ -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<BigEndian>,
offset: U32<BigEndian>,
}
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<u32>) -> 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<R>(&self, reader: &mut R) -> Result<ExpOrData>
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<BigEndian>,
version: U32<BigEndian>,
size: U32<BigEndian>,
reserved: U64<BigEndian>,
/// 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<Self> {
let mut offset: u32 = size_of::<ExchangeFormatV1Hdr>() 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<Self> {
let mut offset: u32 = size_of::<ExchangeFormatV1Hdr>() 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<E: ByteOrder> TryFrom<U32<E>> for ExchangeFormatVersion {
type Error = Error;
fn try_from(value: U32<E>) -> Result<Self, Self::Error> {
if value.get() == Self::One as u32 {
Ok(Self::One)
} else {
bail!(
"{INV_EXCHANGE_FMT_ERROR_TEXT} Unsupported version: ({})",
value.get()
);
}
}
}
impl<E: ByteOrder> From<ExchangeFormatVersion> for U32<E> {
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<u8>),
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<Vec<u8>> {
match self {
ExpOrData::Data(v) => Some(v),
_ => None,
}
}
}
impl From<Option<u32>> for ExpOrData {
fn from(value: Option<u32>) -> Self {
match value {
Some(v) => ExpOrData::Exp(v),
None => ExpOrData::None,
}
}
}
impl From<&ExpOrData> for Option<u32> {
fn from(value: &ExpOrData) -> Self {
match value {
ExpOrData::Exp(v) => Some(*v),
_ => None,
}
}
}
impl From<ExpOrData> for Option<Vec<u8>> {
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<u8>,
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<u8>,
pub measurement: Vec<u8>,
pub additional: Option<Vec<u8>>,
pub user: Option<Vec<u8>>,
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<u8>, exp_measurement: u32, exp_additional: u32) -> Result<Self> {
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<W>(&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<W>(&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<R>(reader: &mut R) -> Result<Self>
where
R: Read + Seek,
{
let mut buf = vec![0; size_of::<ExchangeFormatV1Hdr>()];
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::<ExchangeFormatVersion>::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<S>(seek: &mut S) -> Result<u64>
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<u8>,
measurement: Vec<u8>,
additional: Option<Vec<u8>>,
user: Option<Vec<u8>>,
config_uid: ConfigUid,
) -> Result<Self> {
// 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<W>(&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<W>(&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<R>(reader: &mut R) -> Result<Self>
where
R: Read + Seek,
{
let mut buf = vec![0; size_of::<ExchangeFormatV1Hdr>()];
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::<ExchangeFormatVersion>::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<u8>,
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<u8>,
measurement: Vec<u8>,
additional: Option<Vec<u8>>,
user: Option<Vec<u8>>,
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();
}
}

54
rust/pvattest/src/main.rs Normal file
View File

@@ -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<ExitCode> {
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()),
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.