diff --git a/ga10/rules/keylime/public.go b/ga10/rules/keylime/public.go index 32567e3..35ece9a 100644 --- a/ga10/rules/keylime/public.go +++ b/ga10/rules/keylime/public.go @@ -2,17 +2,21 @@ package keylime import ( "a10/configuration" + "a10/operations" "a10/structures" "bytes" + "encoding/base64" "encoding/json" "fmt" "io" "net/http" + "strings" ) func Registration() []structures.Rule { validateMB := structures.Rule{"keylime_mb", "Checks TPM Measured Boot Log against Keylime", ValidateMB, true} - return []structures.Rule{validateMB} + validateIMA := structures.Rule{"keylime_ima", "Checks TPM Measured Boot Log against Keylime", ValidateIMA, true} + return []structures.Rule{validateMB, validateIMA} } type MBRequest struct { @@ -44,6 +48,11 @@ func postMBRequest(mb MBRequest) (*MBResponse, error) { if err != nil { return nil, err } + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("got invalid response %s", string(respBytes)) + } + var res MBResponse err = json.Unmarshal(respBytes, &res) if err != nil { @@ -53,13 +62,12 @@ func postMBRequest(mb MBRequest) (*MBResponse, error) { } func ValidateMB(claim structures.Claim, rule string, ev structures.ExpectedValue, session structures.Session, parameter map[string]interface{}) (structures.ResultValue, string, error) { - imaLogEncoded, ok := claim.Body["eventlog"] + mbLogEncoded, ok := claim.Body["eventlog"] if !ok { return structures.Fail, "eventlog cannot be found in claim", nil } - fmt.Println(parameter) // Check if required parameters are there - for _, name := range []string{"hash_alg", "pcrs_inquote"} { + for _, name := range []string{"hash_alg", "pcrs_inquote", "pcrscid"} { _, ok := parameter[name] if !ok { return structures.Fail, fmt.Sprintf("parameter is missing %s", name), nil @@ -71,9 +79,15 @@ func ValidateMB(claim structures.Claim, rule string, ev structures.ExpectedValue pcrsInQuote = append(pcrsInQuote, entry.(string)) } + pcrsClaimId := parameter["pcrscid"].(string) + pcrsClaim, err := operations.GetClaimByItemID(pcrsClaimId) + if err != nil { + return structures.Fail, "Could not get PCRs claim", nil + } + pcrs := pcrsClaim.Body[hashAlg].(map[string]interface{}) var decoded structures.KeylimeMBEV - err := decoded.Decode(ev) + err = decoded.Decode(ev) if err != nil { return structures.Fail, "Decoding of EV failed", nil } @@ -81,7 +95,7 @@ func ValidateMB(claim structures.Claim, rule string, ev structures.ExpectedValue req := MBRequest{ AgentID: claim.Header.Element.Name, HashAlg: hashAlg, - MBMeasurementList: imaLogEncoded.(string), + MBMeasurementList: mbLogEncoded.(string), PCRsInQuote: pcrsInQuote, MBRefState: decoded.MBRefstate, } @@ -96,5 +110,120 @@ func ValidateMB(claim structures.Claim, rule string, ev structures.ExpectedValue if err != nil { return structures.Fail, "Failed to encode response", nil } + + // Check if the PCRs also match + for k, v := range resp.MBPCRHashes { + // HACK: demo machines firmware has a miss match for PCR0, that cannot explained via the UEFI eventlog + if k == "0" { + continue + } + other, ok := pcrs[k] + if !ok { + return structures.Fail, fmt.Sprintf("PCR %s is not included output from Keylime", k), nil + } + // Keylime trims leading 0s + data := other.(string) + data = strings.TrimLeft(data, "0") + if data != v { + return structures.Fail, fmt.Sprintf("Mismatch for PCR %s. Got: %s, expected %s", k, other, v), nil + } + } + return structures.Success, string(data), nil } + +type IMARequest struct { + AgentID string `json:"agent_id"` + HashAlg string `json:"hash_alg"` + IMAMeasurementList string `json:"ima_measurement_list"` + RuntimePolicy string `json:"runtime_policy"` + PCRVal string `json:"pcrval"` +} + +type IMAResponse struct { + Failure string `json:"failure"` + Context string `json:"context"` +} + +func postIMARequest(ima IMARequest) (*IMAResponse, error) { + url := configuration.ConfigData.Keylime.ApiUrl + "/ima/validate" + data, err := json.Marshal(ima) + if err != nil { + return nil, err + } + + resp, err := http.Post(url, "application/json", bytes.NewBuffer(data)) + if err != nil { + return nil, err + } + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("got invalid response %s", string(respBytes)) + } + + var res IMAResponse + err = json.Unmarshal(respBytes, &res) + if err != nil { + return nil, err + } + return &res, nil +} + +func ValidateIMA(claim structures.Claim, rule string, ev structures.ExpectedValue, session structures.Session, parameter map[string]interface{}) (structures.ResultValue, string, error) { + ima, ok := claim.Body["asciilog"] + if !ok { + return structures.Fail, "IMA log cannot be found in claim", nil + } + + imaDecoded, err := base64.StdEncoding.DecodeString(ima.(string)) + if err != nil { + return structures.Fail, "Cannot base64 decode IMA log", nil + } + + // Check if required parameters are there + for _, name := range []string{"hash_alg", "pcrscid"} { + _, ok := parameter[name] + if !ok { + return structures.Fail, fmt.Sprintf("parameter is missing %s", name), nil + } + } + + hashAlg := parameter["hash_alg"].(string) + pcrsClaimId := parameter["pcrscid"].(string) + pcrsClaim, err := operations.GetClaimByItemID(pcrsClaimId) + if err != nil { + return structures.Fail, "Could not get PCR claim", nil + } + + pcrs := pcrsClaim.Body[hashAlg].(map[string]interface{}) + pcr10 := pcrs["10"].(string) + + var runtimePolicy structures.KeylimeIMAEV + err = runtimePolicy.Decode(ev) + if err != nil { + return structures.Fail, "Could not decode EV", nil + } + + req := IMARequest{ + AgentID: claim.Header.Element.Name, + HashAlg: hashAlg, + PCRVal: pcr10, + IMAMeasurementList: string(imaDecoded), + RuntimePolicy: runtimePolicy.RuntimePolicy, + } + + resp, err := postIMARequest(req) + if err != nil { + return structures.Fail, fmt.Sprintf("Communication with Keylime failed %w", err), nil + } + + if resp.Failure != "" { + return structures.Fail, fmt.Sprintf("IMA attestation failed: %s, context: %s", resp.Failure, resp.Context), nil + } + + return structures.Success, "", nil +} diff --git a/ga10/rules/tpm2rules/public.go b/ga10/rules/tpm2rules/public.go index e2943f1..7d3aca6 100644 --- a/ga10/rules/tpm2rules/public.go +++ b/ga10/rules/tpm2rules/public.go @@ -2,11 +2,15 @@ package tpm2rules import ( "bytes" + "crypto/sha256" "encoding/base64" "encoding/hex" "fmt" "reflect" + "slices" + "strconv" + "a10/operations" "a10/structures" "a10/utilities" @@ -15,15 +19,16 @@ import ( ) func Registration() []structures.Rule { - attestedPCRDigest := structures.Rule{"tpm2_attestedValue", "Checks the TPM's reported attested value against the expected value", AttestedPCRDigest, true} + checkPCRSelection := structures.Rule{"tpm2_PCRSelection", "Checks if a quote includes the correct PCRs", checkPCRSelection, true} + checkQuoteDigest256 := structures.Rule{"tpm2_quoteDigest256", "Checks if a claim of PCRs match the hash in the quote (sha256)", checkQuoteDigest256, false} ruleFirmware := structures.Rule{"tpm2_firmware", "Checks the TPM firmware version against the expected value", FirmwareRule, true} ruleMagic := structures.Rule{"tpm2_magicNumber", "Checks the quote magic number is 0xFF544347", MagicNumberRule, false} ruleIsSafe := structures.Rule{"tpm2_safe", "Checks that the value of safe is 1", IsSafe, false} ruleValidSignature := structures.Rule{"tpm2_validSignature", "Checks that the signature of rule is valid against the signing attestation key", ValidSignature, false} ruleValidNonce := structures.Rule{"tpm2_validNonce", "Checks that nonce used for the claim matches the nonce in the quote", ValidNonce, false} - return []structures.Rule{ruleFirmware, ruleMagic, attestedPCRDigest, ruleIsSafe, ruleValidSignature, ruleValidNonce} + return []structures.Rule{ruleFirmware, ruleMagic, attestedPCRDigest, ruleIsSafe, ruleValidSignature, ruleValidNonce, checkQuoteDigest256, checkPCRSelection} } func IsSafe(claim structures.Claim, rule string, ev structures.ExpectedValue, session structures.Session, parameter map[string]interface{}) (structures.ResultValue, string, error) { @@ -137,6 +142,88 @@ func ValidNonce(claim structures.Claim, rule string, ev structures.ExpectedValue return structures.Success, "nonce matches", nil } +func checkPCRSelection(claim structures.Claim, rule string, ev structures.ExpectedValue, session structures.Session, parameter map[string]interface{}) (structures.ResultValue, string, error) { + quote, err := getQuote(claim) + if err != nil { + return structures.Fail, "Parsing TPM quote failed", err + } + + data, err := quote.Data.Attested.Quote() + if err != nil { + return structures.Fail, "Parsing Attest structure into Quote failed", err + } + selection := utilities.TPMSPCRSelectionToList(data.PCRSelect.PCRSelections) + + evSelection, ok := ev.EVS["pcrselection"] + if !ok { + return structures.MissingExpectedValue, "pcrselection not given", err + } + var evSelectionList []int + for _, v := range evSelection.(primitive.A) { + index, err := strconv.Atoi(v.(string)) + if err != nil { + return structures.Fail, "pcrselection contains non integer strings", err + } + evSelectionList = append(evSelectionList, index) + } + if len(evSelectionList) != len(selection) { + return structures.Fail, "not the same length", err + } + for _, v := range evSelectionList { + if !slices.Contains(selection, v) { + return structures.Fail, fmt.Sprintf("Index %d is missing in quote", v), err + } + } + return structures.Success, "", err +} + +func checkQuoteDigest256(claim structures.Claim, rule string, ev structures.ExpectedValue, session structures.Session, parameter map[string]interface{}) (structures.ResultValue, string, error) { + quote, err := getQuote(claim) + if err != nil { + return structures.Fail, "Parsing TPM quote failed", err + } + pcrsClaimID := parameter["pcrscid"].(string) + + pcrsClaim, err := operations.GetClaimByItemID(pcrsClaimID) + if err != nil { + return structures.Fail, "Could not get PCRs claim", err + } + + data, err := quote.Data.Attested.Quote() + if err != nil { + return structures.Fail, "Parsing Attest structure into Quote failed", err + } + digest := data.PCRDigest.Buffer + selection := utilities.TPMSPCRSelectionToList(data.PCRSelect.PCRSelections) + + sha256Entries := make(map[string]string) + for k, v := range pcrsClaim.Body["sha256"].(map[string]interface{}) { + sha256Entries[k] = v.(string) + } + + hash := sha256.New() + for _, pcrIndex := range selection { + pcrIndexS := fmt.Sprintf("%d", pcrIndex) + + entry, ok := sha256Entries[pcrIndexS] + if !ok { + return structures.Fail, fmt.Sprintf("PCR index missing in PCR claim: %s", entry), err + } + entryBytes, err := hex.DecodeString(entry) + if err != nil { + return structures.Fail, "Entry not valid hex", err + } + hash.Write(entryBytes) + } + + digestPCRs := hash.Sum([]byte{}) + if !bytes.Equal(digestPCRs, digest) { + return structures.Fail, "PCRs and hash in quote do not match", err + } + + return structures.Success, "PCRs and hash in quote match", err +} + // Constructs AttestableData struct with signature // TODO find way to cache this in the session object func getQuote(claim structures.Claim) (*utilities.AttestableData, error) { diff --git a/ga10/structures/expectedValues.go b/ga10/structures/expectedValues.go index 7672b86..6e542f8 100644 --- a/ga10/structures/expectedValues.go +++ b/ga10/structures/expectedValues.go @@ -22,6 +22,10 @@ type KeylimeMBEV struct { MBRefstate string } +type KeylimeIMAEV struct { + RuntimePolicy string +} + type MarbleRunCoordinatorEV struct { SecurityVersion uint UniqueID []byte @@ -57,6 +61,12 @@ func (e *KeylimeMBEV) Decode(ev ExpectedValue) error { return nil } +func (e *KeylimeIMAEV) Decode(ev ExpectedValue) error { + e.RuntimePolicy = ev.EVS["runtime_policy"].(string) + + return nil +} + func (e *MarbleRunInfrastructureEV) Equal(other MarbleRunInfrastructureEV) bool { return e.UEID == other.UEID && e.CPUSVN == other.CPUSVN && diff --git a/ta10/ima/endpoints.go b/ta10/ima/endpoints.go index c162024..7c7a3a5 100644 --- a/ta10/ima/endpoints.go +++ b/ta10/ima/endpoints.go @@ -1,33 +1,32 @@ package ima -import( - "net/http" +import ( + "encoding/base64" "fmt" "io/ioutil" - "encoding/base64" - - "ta10/common" + "net/http" + utilities "ta10/common" "github.com/labstack/echo/v4" ) -const IMALOGLOCATION string = "/sys/kernel/ima/ascii_runtime_measurements" +const IMALOGLOCATION string = "/sys/kernel/security/ima/ascii_runtime_measurements" type returnASCIILog struct { - ASCIILog string `json:"asciilog"` - Encoding string `json:"encoded"` + ASCIILog string `json:"asciilog"` + Encoding string `json:"encoded"` UnEncodedLength int `json:"unencodedlength"` - EncodedLength int `json:"encodedlength"` + EncodedLength int `json:"encodedlength"` } func GetEventLogLocation(loc string) string { - fmt.Printf("IMA Log requested from %v, unsafe mode is %v, giving: ",loc,utilities.IsUnsafe()) + fmt.Printf("IMA Log requested from %v, unsafe mode is %v, giving: ", loc, utilities.IsUnsafe()) - if utilities.IsUnsafe()==true { - fmt.Printf("%v\n",loc) + if utilities.IsUnsafe() == true { + fmt.Printf("%v\n", loc) return loc } else { - fmt.Printf("%v\n",IMALOGLOCATION) + fmt.Printf("%v\n", IMALOGLOCATION) return IMALOGLOCATION } } @@ -35,24 +34,23 @@ func GetEventLogLocation(loc string) string { func ASCIILog(c echo.Context) error { fmt.Println("ima ascii called") - var postbody map[string]interface{} - var rtnbody = make( map[string]interface{} ) + var postbody map[string]interface{} + var rtnbody = make(map[string]interface{}) - if err := c.Bind(&postbody); err != nil { - rtnbody["postbody"] = err.Error() + if err := c.Bind(&postbody); err != nil { + rtnbody["postbody"] = err.Error() return c.JSON(http.StatusBadRequest, rtnbody) } - u := GetEventLogLocation(fmt.Sprintf("%v",postbody["ima/ASCIIlog"])) - + u := GetEventLogLocation(fmt.Sprintf("%v", postbody["ima/ASCIIlog"])) - fcontent,err := ioutil.ReadFile(u) + fcontent, err := ioutil.ReadFile(u) if err != nil { - rtnbody["file err"]=err.Error() + rtnbody["file err"] = err.Error() return c.JSON(http.StatusInternalServerError, rtnbody) } scontent := base64.StdEncoding.EncodeToString(fcontent) - rtn := returnASCIILog{ scontent, "base64", len(fcontent), len(scontent) } + rtn := returnASCIILog{scontent, "base64", len(fcontent), len(scontent)} return c.JSON(http.StatusOK, rtn) }