diff --git a/attestation-service/verifier/src/se/README.md b/attestation-service/verifier/src/se/README.md index 9d14da24d..0ee09c088 100644 --- a/attestation-service/verifier/src/se/README.md +++ b/attestation-service/verifier/src/se/README.md @@ -1,10 +1,22 @@ -# Deployment of KBS with IBM SE verifier + +# KBS with IBM SE verifier This is a document to guide developer run a KBS with IBM SE verifier locally for development purpose. +## Index + +- [Deployment of KBS with IBM SE verifier](#deployment-of-kbs-with-ibm-se-verifier) +- [Set attestation policy for IBM SE verifier](#set-attestation-policy) + + + +# Deployment of KBS with IBM SE verifier + +This section is about deployment of KBS without rvps checking. + ## Generate RSA keys Generate RSA 4096 key pair following commands: -``` +```bash openssl genrsa -aes256 -passout pass:test1234 -out encrypt_key-psw.pem 4096 openssl rsa -in encrypt_key-psw.pem -passin pass:test1234 -pubout -out encrypt_key.pub openssl rsa -in encrypt_key-psw.pem -out encrypt_key.pem @@ -20,14 +32,25 @@ ibm-z-host-key-signing-gen2.crt DigiCertCA.crt ### CRL -ibm-z-host-key-gen2.crl +ibm-z-host-key-gen2.crl +DigiCertTrustedRootG4.crl +DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crl + +Note: `DigiCertTrustedRootG4.crl` and `DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crl` come from commands as below: +```bash +# openssl x509 -in DigiCertCA.crt --text --noout |grep crl + URI:http://crl3.digicert.com/DigiCertTrustedRootG4.crl +# openssl x509 -in ibm-z-host-key-signing-gen2.crt --text --noout |grep crl + URI:http://crl3.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crl + URI:http://crl4.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crl +``` ## Download HKD Download IBM Secure Execution Host Key Document following: https://www.ibm.com/docs/en/linux-on-z?topic=execution-verify-host-key-document ## Get SE Header Build `se.img` following [Generate an IBM Secure Execution image](https://www.ibm.com/docs/en/linux-on-systems?topic=commands-genprotimg) and retrieve the hdr.bin via command like below. -``` +```bash ./pvextract-hdr -o hdr.bin se.img ``` @@ -35,7 +58,7 @@ Refer [ibm-s390-linux](https://github.com/ibm-s390-linux/s390-tools/blob/v2.33.1 ## Generate KBS key Generate keys used by KBS service. -``` +```bash openssl genpkey -algorithm ed25519 > kbs.key openssl pkey -in kbs.key -pubout -out kbs.pem ``` @@ -43,7 +66,7 @@ openssl pkey -in kbs.key -pubout -out kbs.pem ## (Option 1) Launch KBS as a program - Build KBS -``` +```bash cargo install --locked --debug --path kbs/src/kbs --no-default-features --features coco-as-builtin,openssl,resource,opa ``` @@ -56,6 +79,8 @@ cargo install --locked --debug --path kbs/src/kbs --no-default-features --featur | └── DigiCertCA.crt ├── crls │ └── ibm-z-host-key-gen2.crl +│ └── DigiCertTrustedRootG4.crl +│ └── DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crl ├── hdr │ └── hdr.bin ├── hkds @@ -92,25 +117,24 @@ remote_addr = "" ``` - Launch the KBS program -``` +```bash export RUST_LOG=debug export SE_SKIP_CERTS_VERIFICATION=true ./kbs --config-file ./kbs-config.toml ``` -> Note: `SE_SKIP_CERTS_VERIFICATION=true` only required for a development machine. +> Note: `export SE_SKIP_CERTS_VERIFICATION=true` only required for a development machine. Use `export CERTS_OFFLINE_VERIFICATION=true` to verifiy the certificates offline. ## (Option 2) Launch KBS via docker-compose - Build the docker image ``` -DOCKER_BUILDKIT=1 docker build -t ghcr.io/confidential-containers/staged-images/kbs:latest --build-arg KBS_FEATURES=coco-as-builtin,openssl,resource,opa . -f kbs/docker/Dockerfile +DOCKER_BUILDKIT=1 docker build --build-arg HTTPS_CRYPTO="openssl" --build-arg ARCH="s390x" -t ghcr.io/confidential-containers/staged-images/kbs:latest . -f kbs/docker/Dockerfile ``` ->Note: Please add `--debug` in statement like `cargo install` in file `kbs/docker/Dockerfile` if you're using a development host key document to skip HKD's signature verification. - Prepare a docker compose file, similar as: ``` services: - web: + kbs: image: ghcr.io/confidential-containers/staged-images/kbs:latest command: [ "/usr/local/bin/kbs", @@ -135,7 +159,7 @@ services: - ./data/rsa/encrypt_key.pem:/run/confidential-containers/ibmse/rsa/encrypt_key.pem - ./data/rsa/encrypt_key.pub:/run/confidential-containers/ibmse/rsa/encrypt_key.pub ``` -> Note: `SE_SKIP_CERTS_VERIFICATION=true` only required for a development machine. +> Note: `export SE_SKIP_CERTS_VERIFICATION=true` only required for a development machine. Use `export CERTS_OFFLINE_VERIFICATION=true` to verifiy the certificates offline. - Prepare the material, similar as: ``` @@ -149,6 +173,8 @@ services: │   │   └── DigiCertCA.crt │   ├── crls │   │   └── ibm-z-host-key-gen2.crl +│ │ └── DigiCertTrustedRootG4.crl +│ │ └── DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crl │   ├── hdr.bin │   ├── hkds │   │   └── HKD-3931-0275D38.crt @@ -167,10 +193,63 @@ services: ``` - Launch KBS as docker compose application -``` +```bash docker-compose up -d -docker-compose logs web +docker-compose logs kbs docker-compose down ``` +# Set attestation policy + +This section is about setting attestation policy. + +### Retrive the attestation policy fields for IBM SE + +Using [se_parse_hdr.py](se_parse_hdr.py) on a s390x instance to retrieve the IBM SE fields for attestation policy. + +```bash +python3 se_parse_hdr.py hdr.bin HKD-3931.crt + +... + ================================================ + se.image_phkh: xxx + se.version: 256 + se.tag: xxx + se.attestation_phkh: xxx +``` + +We get following fields and will set these fields in rvps for attestation policy. +`se.version: 256` +`se.tag: xxx` +`se.attestation_phkh: xxx` +`se.image_phkh: xxx` + + +### Set attestation policy + +#### Generate attestation policy file +```bash +cat << EOF > ibmse-policy.rego +package policy +import rego.v1 +default allow = false + +converted_version := sprintf("%v", [input["se.version"]]) + +allow if { + input["se.attestation_phkh"] == "xxx" + input["se.image_phkh"] == "xxx" + input["se.tag"] == "xxx" + input["se.user_data"] == "xxx" + converted_version == "256" +} +EOF +``` + +Where the values `se.version`, `se.attestation_phkh`, `se.image_phkh` and `se.tag` come from [retrive-the-rvps-field-for-an-ibm-se-image](#retrive-the-rvps-field-for-an-ibm-se-image). The value `se.user_data` comes from [initdata](https://github.com/confidential-containers/cloud-api-adaptor/blob/main/src/cloud-api-adaptor/docs/initdata.md). Please remove `input["se.user_data"] == "xxx"` if `initdata` is not used. + +#### Set the attestation policy +```bash +kbs-client --url http://127.0.0.1:8080 config --auth-private-key ./kbs/kbs.key set-attestation-policy --policy-file ./ibmse-policy.rego +``` \ No newline at end of file diff --git a/attestation-service/verifier/src/se/ibmse.rs b/attestation-service/verifier/src/se/ibmse.rs index fc60c4a7e..e8cf22477 100644 --- a/attestation-service/verifier/src/se/ibmse.rs +++ b/attestation-service/verifier/src/se/ibmse.rs @@ -90,8 +90,7 @@ pub struct SeAttestationResponse { pub struct SeAttestationClaims { #[serde_as(as = "Hex")] cuid: ConfigUid, - #[serde_as(as = "Hex")] - user_data: Vec, + user_data: String, version: u32, #[serde_as(as = "Hex")] image_phkh: Vec, @@ -160,12 +159,12 @@ impl SeVerifierImpl { fn encrypt(&self, text: &[u8]) -> Result> { let mut encrypter = Encrypter::new(&self.public_key)?; encrypter.set_rsa_padding(Padding::PKCS1)?; - + let buffer_len = encrypter.encrypt_len(text)?; let mut encrypted = vec![0; buffer_len]; let len = encrypter.encrypt(text, &mut encrypted)?; encrypted.truncate(len); - + Ok(encrypted) } @@ -218,7 +217,7 @@ impl SeVerifierImpl { let claims = SeAttestationClaims { cuid: se_response.cuid, - user_data: se_response.user_data.clone(), + user_data: String::from_utf8(se_response.user_data.clone())?, version: AttestationVersion::One as u32, image_phkh: image_phkh.to_vec(), attestation_phkh: attestation_phkh.to_vec(), @@ -277,22 +276,19 @@ impl SeVerifierImpl { let c = certs .first() .ok_or(anyhow!("File does not contain a X509 certificate"))?; - #[cfg(debug_assertions)] - { - const DEFAULT_SE_SKIP_CERTS_VERIFICATION: &str = "false"; - let skip_certs_env = env_or_default!( - "SE_SKIP_CERTS_VERIFICATION", - DEFAULT_SE_SKIP_CERTS_VERIFICATION - ); - let skip_certs: bool = skip_certs_env.parse::().unwrap_or(false); - if !skip_certs { - let verifier = CertVerifier::new(ca_certs.as_slice(), crls.as_slice(), ca_option.clone(), offline_certs_verify)?; - verifier.verify(c)?; - } - } - #[cfg(not(debug_assertions))] - { - let verifier = CertVerifier::new(ca_certs.as_slice(), crls.as_slice(), ca_option.clone(), offline_certs_verify)?; + const DEFAULT_SE_SKIP_CERTS_VERIFICATION: &str = "false"; + let skip_certs_env = env_or_default!( + "SE_SKIP_CERTS_VERIFICATION", + DEFAULT_SE_SKIP_CERTS_VERIFICATION + ); + let skip_certs: bool = skip_certs_env.parse::().unwrap_or(false); + if !skip_certs { + let verifier = CertVerifier::new( + ca_certs.as_slice(), + crls.as_slice(), + ca_option.clone(), + offline_certs_verify, + )?; verifier.verify(c)?; } arcb.add_hostkey(c.public_key()?); @@ -301,8 +297,7 @@ impl SeVerifierImpl { let encr_ctx = ReqEncrCtx::random(SymKeyType::Aes256)?; let request_blob = arcb.encrypt(&encr_ctx)?; let conf_data = arcb.confidential_data(); - let encr_measurement_key = - self.encrypt(conf_data.measurement_key())?; + let encr_measurement_key = self.encrypt(conf_data.measurement_key())?; let nonce = conf_data .nonce() .as_ref() diff --git a/attestation-service/verifier/src/se/mod.rs b/attestation-service/verifier/src/se/mod.rs index fe10b0245..7fef50c2a 100644 --- a/attestation-service/verifier/src/se/mod.rs +++ b/attestation-service/verifier/src/se/mod.rs @@ -38,13 +38,12 @@ impl Verifier for SeVerifier { se_verifier.evaluate(evidence) } - async fn generate_supplemental_challenge( - &self, - _tee_parameters: String, - ) -> Result { + async fn generate_supplemental_challenge(&self, _tee_parameters: String) -> Result { let se_verifier = VERIFIER .get_or_try_init(|| async { SeVerifierImpl::new() }) .await?; - se_verifier.generate_supplemental_challenge(_tee_parameters).await + se_verifier + .generate_supplemental_challenge(_tee_parameters) + .await } } diff --git a/attestation-service/verifier/src/se/se_parse_hdr.py b/attestation-service/verifier/src/se/se_parse_hdr.py new file mode 100644 index 000000000..d8191f664 --- /dev/null +++ b/attestation-service/verifier/src/se/se_parse_hdr.py @@ -0,0 +1,162 @@ +# Copyright (C) Copyright IBM Corp. 2024 +# +# SPDX-License-Identifier: Apache-2.0 +# + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization, hashes +from cryptography.hazmat.primitives.asymmetric import ec +import hashlib +import sys +import struct + + +def parse_certificate(cert_path): + """Parse the certificate from file path and return the public key.""" + with open(cert_path, 'rb') as cert_file: + cert_data = cert_file.read() + cert = x509.load_pem_x509_certificate(cert_data, default_backend()) + return cert.public_key() + +def ec_point_to_affine_coordinates(public_key): + """Convert EC public key to affine coordinates (x, y).""" + if isinstance(public_key, ec.EllipticCurvePublicKey): + # Get the uncompressed point bytes + point = public_key.public_bytes( + encoding=serialization.Encoding.X962, + format=serialization.PublicFormat.UncompressedPoint + ) + curve = public_key.curve + x_bytes = point[1:curve.key_size//8+2] # skip the first byte (0x04) + y_bytes = point[curve.key_size//8+2:] + return x_bytes, y_bytes + else: + raise ValueError("Invalid EC public key type") + +def bn_bn2binpad(bn, size): + """Convert BN to binary padded format.""" + bn_bytes = bn.to_bytes((bn.bit_length() + 7) // 8, byteorder='big') + if len(bn_bytes) < size: + padded_bytes = b'\x00' * (size - len(bn_bytes)) + bn_bytes + elif len(bn_bytes) > size: + padded_bytes = bn_bytes[-size:] + else: + padded_bytes = bn_bytes + return padded_bytes + +def generate_sha256_hash(data): + """Generate SHA-256 hash of input data.""" + sha256_hash = hashlib.sha256(data).hexdigest() + return sha256_hash + +def bytes_to_hex_string(byte_data): + """Convert bytes to hex string.""" + return ''.join(f'{b:02x}' for b in byte_data) + +def parse_img_phkh_from_hkd(filename): + # Parse certificate and extract public key + public_key = parse_certificate(filename) + + # Get affine coordinates + x_bytes, y_bytes = ec_point_to_affine_coordinates(public_key) + + # Convert x_bytes and y_bytes to binary padded format + x_bin = bn_bn2binpad(int.from_bytes(x_bytes, byteorder='big'), 80) # 66 bytes for P-521 curve + y_bin = bn_bn2binpad(int.from_bytes(y_bytes, byteorder='big'), 80) + + # Log x_bin and y_bin + x_bin_str = bytes_to_hex_string(x_bin) + y_bin_str = bytes_to_hex_string(y_bin) + + # Concatenate x_bin and y_bin + ecdh_data = x_bin + y_bin + + # Log concatenated data + ecdh_data_str = bytes_to_hex_string(ecdh_data) + + # Calculate SHA-256 hash + hkd_phkh = generate_sha256_hash(ecdh_data) + return hkd_phkh + +def parse_hdr(hdr_file, hkd_file): + with open(hdr_file, 'rb') as f: + + hkd_phkh = parse_img_phkh_from_hkd(hkd_file) + key_slot_used_idx = -1 + + # Read the entire header based on the size defined in the structure + # https://github.com/ibm-s390-linux/s390-tools/blob/master/genprotimg/src/include/pv_hdr_def.h + header_size = 8 + 4 + 4 + # pv_hdr_head size 416 + pv_hdr_head_size = 8 + 4 + 4 + 12 + 4 + 8 + 8 + 8 + 8 + 160 + 64 + 64 + 64 + + after_key_slot_size = 144 + # pv_hdr_key_slot digest_key + wrapped_key = phkh + phkh_size = 32 + + hdr_data = f.read(header_size) + + # Unpack the header fields + fields = struct.unpack('8sII', hdr_data) + + magic, version, phs = fields + # The last 16 bits is the image tag + f.seek(-16, 2) + image_tag = f.read(16) + # Print the extracted fields + + print(f"Magic: {magic.decode('ascii')}") + print(f"phs: {phs}") + + f.seek(pv_hdr_head_size) + + length_phkh_data = phs - pv_hdr_head_size - after_key_slot_size + phkh_data = f.read(length_phkh_data) + + # Define the struct format (32 bytes for digest_key, 32 bytes for wrapped_key, 16 bytes for tag) + struct_format = '32s32s16s' + + # Calculate the size of each struct + struct_size = struct.calcsize(struct_format) + + for i in range(0, len(phkh_data), struct_size): + if i + struct_size > len(phkh_data): + break + chunk = phkh_data[i:i + struct_size] + digest_key, wrapped_key, tag = struct.unpack(struct_format, chunk) + if digest_key.hex() == hkd_phkh: + key_slot_used_idx = i + print(f" ========Host Key Document Hash used in this slot========= ") + print(f" Key Slot: {i//80 + 1}:") + print(f" image_phkh: {digest_key.hex()}") + print(f" wrapped_key: {wrapped_key.hex()}") + print(f" tag: {tag.hex()}") + # if the 1 slot selected, the idx is 0 + if key_slot_used_idx > -1: + chunk_used = phkh_data[key_slot_used_idx:key_slot_used_idx + struct_size] + digest_key, wrapped_key, tag = struct.unpack(struct_format, chunk_used) + print(f" ========Host Key Document Hash used in this slot========= ") + print(f" Key Slot: {key_slot_used_idx//80 + 1}:") + print(f" wrapped_key: {wrapped_key.hex()}") + print(f" HKD tag: {tag.hex()}") + print(f" Copy below value and set in rvps ") + print(f" ================================================ ") + print(f" se.image_phkh: {digest_key.hex()}") + else: + print(f" The HKD file not included when build the SE image ") + + + print(f" se.version: {version}") + print(f" se.tag: {image_tag.hex()}") + print(f" se.attestation_phkh: {hkd_phkh}") + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + + hdr_file = sys.argv[1] + hkd_file = sys.argv[2] + parse_hdr(hdr_file, hkd_file)