mirror of
https://github.com/ibm-s390-linux/s390-tools.git
synced 2026-08-05 02:14:52 +00:00
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:
15
rust/Cargo.lock
generated
15
rust/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -3,6 +3,7 @@ members = [
|
||||
"pv",
|
||||
"pv_core",
|
||||
"pvapconfig",
|
||||
"pvattest",
|
||||
"pvsecret",
|
||||
"utils",
|
||||
]
|
||||
|
||||
17
rust/pvattest/Cargo.toml
Normal file
17
rust/pvattest/Cargo.toml
Normal 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
261
rust/pvattest/README.md
Normal 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>
|
||||
132
rust/pvattest/man/pvattest-create.1
Normal file
132
rust/pvattest/man/pvattest-create.1
Normal 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)
|
||||
71
rust/pvattest/man/pvattest-perform.1
Normal file
71
rust/pvattest/man/pvattest-perform.1
Normal 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)
|
||||
124
rust/pvattest/man/pvattest-verify.1
Normal file
124
rust/pvattest/man/pvattest-verify.1
Normal 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)
|
||||
114
rust/pvattest/man/pvattest.1
Normal file
114
rust/pvattest/man/pvattest.1
Normal 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-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)
|
||||
257
rust/pvattest/src/cli.rs
Normal file
257
rust/pvattest/src/cli.rs
Normal 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
33
rust/pvattest/src/cmd.rs
Normal 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::*;
|
||||
67
rust/pvattest/src/cmd/create.rs
Normal file
67
rust/pvattest/src/cmd/create.rs
Normal 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)
|
||||
}
|
||||
54
rust/pvattest/src/cmd/perform.rs
Normal file
54
rust/pvattest/src/cmd/perform.rs
Normal 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)
|
||||
}
|
||||
124
rust/pvattest/src/cmd/verify.rs
Normal file
124
rust/pvattest/src/cmd/verify.rs
Normal 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)
|
||||
}
|
||||
884
rust/pvattest/src/exchange.rs
Normal file
884
rust/pvattest/src/exchange.rs
Normal 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
54
rust/pvattest/src/main.rs
Normal 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()),
|
||||
}
|
||||
}
|
||||
BIN
rust/pvattest/tests/assets/exp/exchange/add_req.bin
Normal file
BIN
rust/pvattest/tests/assets/exp/exchange/add_req.bin
Normal file
Binary file not shown.
BIN
rust/pvattest/tests/assets/exp/exchange/add_resp.bin
Normal file
BIN
rust/pvattest/tests/assets/exp/exchange/add_resp.bin
Normal file
Binary file not shown.
BIN
rust/pvattest/tests/assets/exp/exchange/full.bin
Normal file
BIN
rust/pvattest/tests/assets/exp/exchange/full.bin
Normal file
Binary file not shown.
BIN
rust/pvattest/tests/assets/exp/exchange/full_req.bin
Normal file
BIN
rust/pvattest/tests/assets/exp/exchange/full_req.bin
Normal file
Binary file not shown.
BIN
rust/pvattest/tests/assets/exp/exchange/full_resp.bin
Normal file
BIN
rust/pvattest/tests/assets/exp/exchange/full_resp.bin
Normal file
Binary file not shown.
BIN
rust/pvattest/tests/assets/exp/exchange/min_req.bin
Normal file
BIN
rust/pvattest/tests/assets/exp/exchange/min_req.bin
Normal file
Binary file not shown.
BIN
rust/pvattest/tests/assets/exp/exchange/min_resp.bin
Normal file
BIN
rust/pvattest/tests/assets/exp/exchange/min_resp.bin
Normal file
Binary file not shown.
BIN
rust/pvattest/tests/assets/exp/exchange/user_req.bin
Normal file
BIN
rust/pvattest/tests/assets/exp/exchange/user_req.bin
Normal file
Binary file not shown.
BIN
rust/pvattest/tests/assets/exp/exchange/user_resp.bin
Normal file
BIN
rust/pvattest/tests/assets/exp/exchange/user_resp.bin
Normal file
Binary file not shown.
Reference in New Issue
Block a user