diff --git a/README.md b/README.md index d07a092..79b8cfc 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,11 @@ based on a set of trusted CAs and signed metadata describing the software stack. ## Prerequisites - A Linux platform -- For TPM attestation, access to `/dev/tpm0`. -- For AMD SEV-SNP an SNP-capable AMD server +- For TPM attestation, access to `/dev/tpmrm0` or `/dev/tpm0`. +- For AMD SEV-SNP an SNP-capable AMD server and an SNP VM with access to `/dev/sev-guest` - Building the *cmcd* requires *go* (https://golang.org/doc/install) +- Building AWS Firmware (OVMF) for running the CMC within AWS AMD SEV-SNP virtual machines +requires [Nix](https://nixos.org/download/) ## Quick Start diff --git a/attestationreport/validationreport.go b/attestationreport/validationreport.go index 811ba89..f3b353a 100644 --- a/attestationreport/validationreport.go +++ b/attestationreport/validationreport.go @@ -109,10 +109,11 @@ type TpmResult struct { } type SnpResult struct { - VersionMatch Result `json:"reportVersionMatch"` - FwCheck VersionCheck `json:"fwCheck"` - TcbCheck TcbCheck `json:"tcbCheck"` - PolicyCheck PolicyCheck `json:"policyCheck"` + VersionMatch Result `json:"reportVersionMatch"` + FwCheck VersionCheck `json:"fwCheck"` + TcbCheck TcbCheck `json:"tcbCheck"` + PolicyCheck PolicyCheck `json:"policyCheck"` + ExtensionsCheck []Result `json:"extensionsCheck"` } type SgxResult struct { @@ -210,10 +211,9 @@ type TdAttributesCheck struct { // SignatureResults represents the results for validation of // a provided signature and the used certificates. type SignatureResult struct { - SignCheck Result `json:"signatureVerification"` // Result from checking the signature has been calculated with this certificate - CertChainCheck Result `json:"certChainValidation"` // Result from validatint the certification chain back to a shared root of trust - ExtensionsCheck []Result `json:"extensionsCheck,omitempty"` - ValidatedCerts [][]X509CertExtracted `json:"validatedCerts"` //Stripped information from validated x509 cert chain(s) for additional checks from the policies module + SignCheck Result `json:"signatureVerification"` // Result from checking the signature has been calculated with this certificate + CertChainCheck Result `json:"certChainValidation"` // Result from validatint the certification chain back to a shared root of trust + ValidatedCerts [][]X509CertExtracted `json:"validatedCerts"` //Stripped information from validated x509 cert chain(s) for additional checks from the policies module } // X509CertExtracted represents a x509 certificate with attributes @@ -677,9 +677,6 @@ func (r *Result) PrintErr(format string, args ...interface{}) { func (r *SignatureResult) PrintErr(format string, args ...interface{}) { r.SignCheck.PrintErr("%v signature check", fmt.Sprintf(format, args...)) r.CertChainCheck.PrintErr("%v cert chain check", fmt.Sprintf(format, args...)) - for _, e := range r.ExtensionsCheck { - e.PrintErr("%v extension check", fmt.Sprintf(format, args...)) - } } func (r *VerificationResult) PrintErr() { diff --git a/example-setup/setup-cmc-snp b/example-setup/setup-cmc-snp index db08453..c67b673 100755 --- a/example-setup/setup-cmc-snp +++ b/example-setup/setup-cmc-snp @@ -37,7 +37,15 @@ mkdir -p "${data}" sudo apt install -y moreutils golang-cfssl build-essential zlib1g-dev libssl-dev jq # Install virtee sev-snp measure tools -git clone https://github.com/virtee/sev-snp-measure "${data}/" +echo "Clone virtee/sev-snp-measure tools" +git clone https://github.com/virtee/sev-snp-measure.git "${data}/sev-snp-measure" + +# Clone OVMF with AWS patches +echo "Cloning AWS OVMF.." +git clone https://github.com/aws/uefi.git "${data}/uefi" +cd "${data}/uefi" +echo "Building AWS OVMF.." +nix-build --pure # Build CMC cd "${cmc}" diff --git a/example-setup/update-platform-snp b/example-setup/update-platform-snp index e48685d..e34520b 100755 --- a/example-setup/update-platform-snp +++ b/example-setup/update-platform-snp @@ -8,7 +8,7 @@ source "${dir}/utils.sh" export PATH=${PATH}:${HOME}/go/bin if [[ "$#" -ne 2 ]]; then - echo "Usage: ./update-platform " + echo "Usage: ./update-platform " exit 1 fi @@ -16,8 +16,9 @@ data=$(set -e; abs_path "${1}") input="${data}/metadata-raw" tmp="${data}/metadata-tmp" out="${data}/metadata-signed" -ovmf="${2}" -ser="${3}" +ser="${2}" + +ovmf="${data}/uefi/result/ovmf_img.fd" if [[ ! -d "${data}" ]]; then echo "Data directory ${1} does not exist. Did you run the setup-cmc script? Abort.." @@ -44,14 +45,18 @@ printf "%s\n" "${json}" > "${input}/device.description.json" manifestjson=$(cat "${input}/rtm.manifest.json") # Insert high-level details -manifestjson=$(echo "${json}" | jq ".details.firmware = \"${firmware}\"") -manifestjson=$(echo "${json}" | jq ".details.bootloader = \"${bootloader}\"") +manifestjson=$(echo "${manifestjson}" | jq ".details.firmware = \"${firmware}\"") +manifestjson=$(echo "${manifestjson}" | jq ".details.bootloader = \"${bootloader}\"") json=$(cat "${dir}/metadata-raw/snp.referencevalue.template") # TODO parameters vor vcpus and vmm-type ref=$("${dir}/sev-snp-measure/sev-snp-measure.py" --mode snp --vcpus=2 --vmm-type=ec2 --ovmf="${ovmf}") +# Get AMD cert chain for milan +wget -O "${dir}/cert_chain" https://kdsintf.amd.com/vlek/v1/Milan/cert_chain +awk '/-----BEGIN CERTIFICATE-----/{n++} n==2' "${dir}/cert_chain" > "${dir}/ark_milan.pem" + fingerprint=$(openssl x509 -in "${dir}"/ark_milan.pem -fingerprint -noout -sha256 | sed 's/://g' | cut -d "=" -f2) # Insert reference values diff --git a/snpdriver/snpdriver.go b/snpdriver/snpdriver.go index cc26382..6765402 100644 --- a/snpdriver/snpdriver.go +++ b/snpdriver/snpdriver.go @@ -50,12 +50,6 @@ const ( DER ) -type SigningKey int - -const ( - VCEK = iota - VLEK -) const ( snpChainFile = "akchain.pem" signingChainFile = "ikchain.pem" @@ -325,16 +319,9 @@ func getSnpCertChain(addr string) ([]*x509.Certificate, error) { return nil, fmt.Errorf("failed to decode SNP report: %w", err) } - arkey := (s.KeySelection >> 2) & 0x7 - var signingKey SigningKey - if arkey == 0 { - log.Trace("VCEK is used to sign attestation report") - signingKey = VCEK - } else if arkey == 1 { - log.Trace("VLEK is used to sign attestation report") - signingKey = VLEK - } else { - return nil, fmt.Errorf("could not determine SNP attestation report signing key") + akType, err := verify.GetAkType(s.KeySelection) + if err != nil { + return nil, fmt.Errorf("could not determine SNP attestation report attestation key") } // TODO mandate server authentication in the future, otherwise @@ -344,7 +331,7 @@ func getSnpCertChain(addr string) ([]*x509.Certificate, error) { var signingCert *x509.Certificate var caUrl string - if signingKey == VCEK { + if akType == verify.VCEK { // VCEK is used, simply request EST enrollment for SNP chip ID and TCB log.Trace("Enrolling VCEK via EST") signingCert, err = client.SnpEnroll(addr, s.ChipId, s.CurrentTcb) @@ -352,7 +339,7 @@ func getSnpCertChain(addr string) ([]*x509.Certificate, error) { return nil, fmt.Errorf("failed to enroll SNP: %w", err) } caUrl = milanUrlVcek - } else if signingKey == VLEK { + } else if akType == verify.VLEK { // VLEK is used, in this case we fetch the VLEK from the host vlek, err := getVlek() if err != nil { diff --git a/verify/snp.go b/verify/snp.go index acc7bfd..2a87188 100644 --- a/verify/snp.go +++ b/verify/snp.go @@ -80,6 +80,14 @@ const ( signature_offset = 0x2A0 ) +type AkType int + +const ( + UNKNOWN = iota + VCEK + VLEK +) + func verifySnpMeasurements(snpM ar.Measurement, nonce []byte, referenceValues []ar.ReferenceValue, ) (*ar.MeasurementResult, bool) { @@ -181,61 +189,59 @@ func verifySnpMeasurements(snpM ar.Measurement, nonce []byte, referenceValues [] }) } - // Compare SNP parameters - result.SnpResult.VersionMatch, ret = verifySnpVersion(s, snpReferenceValue.Snp.Version) + // Verify the SNP report version + result.SnpResult.VersionMatch, ret = verifySnpVersion(s.Version, snpReferenceValue.Snp.Version) if !ret { ok = false } - result.SnpResult.PolicyCheck, ret = verifySnpPolicy(s, snpReferenceValue.Snp.Policy) + // Verify SNP VM configuration + result.SnpResult.PolicyCheck, ret = verifySnpPolicy(s.Policy, snpReferenceValue.Snp.Policy) if !ret { ok = false } + // Verify the SNP firmware version result.SnpResult.FwCheck, ret = verifySnpFw(s, snpReferenceValue.Snp.Fw) if !ret { ok = false } + // Verify the SNP TCB against the specified minimum versions result.SnpResult.TcbCheck, ret = verifySnpTcb(s, snpReferenceValue.Snp.Tcb) if !ret { ok = false } + // Examine SNP x509 extensions + result.SnpResult.ExtensionsCheck, ret = verifySnpExtensions(certs[0], &s) + if !ret { + ok = false + } result.Summary.Success = ok return result, ok } -func DecodeSnpReport(report []byte) (snpreport, error) { - var s snpreport - b := bytes.NewBuffer(report) - err := binary.Read(b, binary.LittleEndian, &s) - if err != nil { - return snpreport{}, fmt.Errorf("failed to decode SNP report: %w", err) - } - return s, nil -} - -func verifySnpVersion(s snpreport, version uint32) (ar.Result, bool) { +func verifySnpVersion(expected, got uint32) (ar.Result, bool) { r := ar.Result{} - ok := s.Version == version + ok := expected == got if !ok { - log.Tracef("SNP report version mismatch: Report = %v, supplied = %v", s.Version, version) + log.Tracef("SNP report version mismatch: Report = %v, supplied = %v", got, expected) r.Success = false - r.Expected = strconv.FormatUint(uint64(version), 10) - r.Got = strconv.FormatUint(uint64(s.Version), 10) + r.Expected = strconv.FormatUint(uint64(expected), 10) + r.Got = strconv.FormatUint(uint64(got), 10) } else { r.Success = true } return r, ok } -func verifySnpPolicy(s snpreport, v ar.SnpPolicy) (ar.PolicyCheck, bool) { +func verifySnpPolicy(policy uint64, v ar.SnpPolicy) (ar.PolicyCheck, bool) { - abiMajor := uint8(s.Policy & 0xFF) - abiMinor := uint8((s.Policy >> 8) & 0xFF) - smt := (s.Policy & (1 << 16)) != 0 - migration := (s.Policy & (1 << 18)) != 0 - debug := (s.Policy & (1 << 19)) != 0 - singleSocket := (s.Policy & (1 << 20)) != 0 + abiMajor := uint8(policy & 0xFF) + abiMinor := uint8((policy >> 8) & 0xFF) + smt := (policy & (1 << 16)) != 0 + migration := (policy & (1 << 18)) != 0 + debug := (policy & (1 << 19)) != 0 + singleSocket := (policy & (1 << 20)) != 0 // Convert to int, as json.Marshal otherwise interprets the values as strings r := ar.PolicyCheck{ @@ -302,6 +308,8 @@ func verifySnpFw(s snpreport, v ar.SnpFw) (ar.VersionCheck, bool) { func verifySnpTcb(s snpreport, v ar.SnpTcb) (ar.TcbCheck, bool) { + // TODO refactor into function and use it also in + // extention function currBl := uint8(s.CurrentTcb & 0xFF) commBl := uint8(s.CommittedTcb & 0xFF) launBl := uint8(s.LaunchTcb & 0xFF) @@ -390,13 +398,6 @@ func verifySnpSignature( s := new(big.Int) s.SetBytes(sRaw) - // Examine SNP x509 extensions - extensionResults, ok := verifySnpExtensions(certs[0], &report) - result.ExtensionsCheck = extensionResults - if !ok { - return result, false - } - // Check that the algorithm is supported if report.SignatureAlgo != ecdsa384_with_sha384 { log.Tracef("Signature Algorithm %v not supported", report.SignatureAlgo) @@ -465,47 +466,109 @@ func verifySnpSignature( return result, true } +const ( + oidBl = "1.3.6.1.4.1.3704.1.3.1" + oidTee = "1.3.6.1.4.1.3704.1.3.2" + oidSnp = "1.3.6.1.4.1.3704.1.3.3" + oidUcode = "1.3.6.1.4.1.3704.1.3.8" + oidChipId = "1.3.6.1.4.1.3704.1.4" + oidCspId = "1.3.6.1.4.1.3704.1.5" +) + +func oidDesc(oid string) string { + switch oid { + case oidBl: + return "OID BL SPL" + case oidTee: + return "OID TEE SPL" + case oidSnp: + return "OID SNP SPL" + case oidUcode: + return "OID uCode SPL" + case oidChipId: + return "OID CHIP ID" + case oidCspId: + return "OID CSP ID" + default: + return "OID UNKNOWN" + } +} + func verifySnpExtensions(cert *x509.Certificate, report *snpreport) ([]ar.Result, bool) { success := true var ok bool var r ar.Result - tcb := report.CurrentTcb + + // Checked extensions depend on the key type + akType, err := GetAkType(report.KeySelection) + if err != nil { + log.Tracef("Could not determine SNP attestation report attestation key type") + success = false + } + + // The x509 extensions must match the reported TCB + tcb := report.ReportedTcb results := make([]ar.Result, 0) - if r, ok = checkExtensionUint8(cert, "1.3.6.1.4.1.3704.1.3.1", uint8(tcb)); !ok { - log.Tracef("SEV BL Extension Check failed:") + if r, ok = checkExtensionUint8(cert, oidBl, uint8(tcb)); !ok { + log.Tracef("SEV BL extension check failed") success = false } results = append(results, r) - if r, ok = checkExtensionUint8(cert, "1.3.6.1.4.1.3704.1.3.2", uint8(tcb>>8)); !ok { - log.Tracef("SEV TEE Extension Check failed") + if r, ok = checkExtensionUint8(cert, oidTee, uint8(tcb>>8)); !ok { + log.Tracef("SEV TEE extension check failed") success = false } results = append(results, r) - if r, ok = checkExtensionUint8(cert, "1.3.6.1.4.1.3704.1.3.3", uint8(tcb>>48)); !ok { - log.Tracef("SEV SNP Extension Check failed") + if r, ok = checkExtensionUint8(cert, oidSnp, uint8(tcb>>48)); !ok { + log.Tracef("SEV SNP extension check failed") success = false } results = append(results, r) - if r, ok = checkExtensionUint8(cert, "1.3.6.1.4.1.3704.1.3.8", uint8(tcb>>56)); !ok { - log.Tracef("SEV UCODE Extension Check failed") + if r, ok = checkExtensionUint8(cert, oidUcode, uint8(tcb>>56)); !ok { + log.Tracef("SEV UCODE extension check failed") success = false } results = append(results, r) - if r, ok = checkExtensionBuf(cert, "1.3.6.1.4.1.3704.1.4", report.ChipId[:]); !ok { - log.Tracef("Chip ID Extension Check failed") - success = false + if akType == VCEK { + // If the VCEK was used, we must compare the reported Chip ID against the extension Chip ID + if r, ok = checkExtensionBuf(cert, oidChipId, report.ChipId[:]); !ok { + log.Tracef("Chip ID extension check failed") + success = false + } + results = append(results, r) + } + + if akType == VLEK { + // If the VLEK was used, the CSP extensions must be present + // TODO currently we simply accept all CSPs, discuss if we need to match here + csp, ok := getExtensionString(cert, oidCspId) + if !ok { + log.Trace("CSP ID extension check failed") + success = false + } + log.Tracef("CSP ID extension present: %v", csp) + results = append(results, ar.Result{Success: ok, Got: csp}) } - results = append(results, r) return results, success } +func DecodeSnpReport(report []byte) (snpreport, error) { + var s snpreport + b := bytes.NewBuffer(report) + err := binary.Read(b, binary.LittleEndian, &s) + if err != nil { + return snpreport{}, fmt.Errorf("failed to decode SNP report: %w", err) + } + return s, nil +} + func min(v []uint8) uint8 { if len(v) == 0 { return 0 @@ -533,3 +596,15 @@ func checkMinVersion(version []uint8, ref []uint8) bool { } return true } + +func GetAkType(keySelection uint32) (AkType, error) { + arkey := (keySelection >> 2) & 0x7 + if arkey == 0 { + log.Trace("VCEK is used to sign attestation report") + return VCEK, nil + } else if arkey == 1 { + log.Trace("VLEK is used to sign attestation report") + return VLEK, nil + } + return UNKNOWN, fmt.Errorf("unknown AK type %v", arkey) +} diff --git a/verify/tpm_test.go b/verify/tpm_test.go index d3591aa..85e81f9 100644 --- a/verify/tpm_test.go +++ b/verify/tpm_test.go @@ -648,9 +648,8 @@ var ( }, }, }, - SignCheck: validResult, - CertChainCheck: validResult, - ExtensionsCheck: nil, + SignCheck: validResult, + CertChainCheck: validResult, } validTpmMeasurementResult = ar.MeasurementResult{ diff --git a/verify/verify.go b/verify/verify.go index ff5d7f0..c64cfe1 100644 --- a/verify/verify.go +++ b/verify/verify.go @@ -618,7 +618,7 @@ func checkExtensionUint8(cert *x509.Certificate, oid string, value uint8) (ar.Re oid, ext.Value[2], value) return ar.Result{ Success: false, - Expected: strconv.FormatUint(uint64(value), 10), + Expected: fmt.Sprintf("%v: %v", oidDesc(oid), strconv.FormatUint(uint64(value), 10)), Got: strconv.FormatUint(uint64(ext.Value[2]), 10), }, false } @@ -631,7 +631,7 @@ func checkExtensionUint8(cert *x509.Certificate, oid string, value uint8) (ar.Re oid, ext.Value[2], ext.Value[3], value) return ar.Result{ Success: false, - Expected: strconv.FormatUint(uint64(value), 10), + Expected: fmt.Sprintf("%v: %v", oidDesc(oid), strconv.FormatUint(uint64(value), 10)), Got: strconv.FormatUint(uint64(ext.Value[3]), 10), }, false } @@ -640,7 +640,10 @@ func checkExtensionUint8(cert *x509.Certificate, oid string, value uint8) (ar.Re oid, ext.Value[1]) return ar.Result{Success: false, ErrorCode: ar.OidLength}, false } - return ar.Result{Success: true}, true + return ar.Result{ + Success: true, + Got: fmt.Sprintf("%v: %v", oidDesc(oid), strconv.FormatUint(uint64(value), 10)), + }, true } } @@ -658,11 +661,14 @@ func checkExtensionBuf(cert *x509.Certificate, oid string, buf []byte) (ar.Resul oid, hex.EncodeToString(ext.Value), hex.EncodeToString(buf)) return ar.Result{ Success: false, - Expected: hex.EncodeToString(buf), + Expected: fmt.Sprintf("%v: %v", oidDesc(oid), hex.EncodeToString(buf)), Got: hex.EncodeToString(ext.Value), }, false } - return ar.Result{Success: true}, true + return ar.Result{ + Success: true, + Got: fmt.Sprintf("%v: %v", oidDesc(oid), hex.EncodeToString(ext.Value)), + }, true } } @@ -670,6 +676,17 @@ func checkExtensionBuf(cert *x509.Certificate, oid string, buf []byte) (ar.Resul return ar.Result{Success: false, ErrorCode: ar.OidNotPresent}, false } +func getExtensionString(cert *x509.Certificate, oid string) (string, bool) { + + for _, ext := range cert.Extensions { + if ext.Id.String() == oid { + return string(ext.Value), true + } + } + log.Tracef("extension %v: %v not present in certificate", oid, oidDesc(oid)) + return "", false +} + func contains(elem string, list []string) bool { for _, s := range list { if s == elem {