diff --git a/api/api.gen.go b/api/api.gen.go index 32b3fb7..e4b4eea 100644 --- a/api/api.gen.go +++ b/api/api.gen.go @@ -452,23 +452,24 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "TT0c//4PAAD//2JvGhaSDQAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/server.go b/api/server.go index 1dfea25..53838df 100644 --- a/api/server.go +++ b/api/server.go @@ -8,13 +8,16 @@ import ( "encoding/json" "fmt" "io" + "mime" "net/http" + "strings" "github.com/moogar0880/problems" "github.com/veraison/cmw" "github.com/veraison/ratsd/plugin" "github.com/veraison/ratsd/proto/compositor" ratsdtoken "github.com/veraison/ratsd/ratsd-token" + ratsdtokenv2 "github.com/veraison/ratsd/ratsd-token-v2" "go.uber.org/zap" ) @@ -23,6 +26,9 @@ const ( ApplicationvndVeraisonCharesJson string = "application/vnd.veraison.chares+json" JsonType string = "application/json" nonceAdjustFunction string = ratsdtoken.NonceAdjustFunctionShake256 + legacyCharesResponseMediaType string = `application/eat-ucs+json; eat_profile="tag:github.com,2024:veraison/ratsd"` + v2CharesResponseMediaType string = `application/cmw+cbor; cmwct="tag:github.com,2026:veraison/ratsd/v2"` + legacyCMWCollectionType string = "tag:github.com,2025:veraison/ratsd/cmw" ) type Server struct { @@ -31,6 +37,18 @@ type Server struct { options string } +type charesResponseFormat int + +const ( + charesResponseLegacy charesResponseFormat = iota + charesResponseV2 +) + +type charesResponse struct { + format charesResponseFormat + contentType string +} + func responseCodeToHTTP(responseCode uint32) int { // Plugin should return 200 on success, 400 for caller input errors, and 500 for everything else. switch responseCode { @@ -73,6 +91,72 @@ func adjustNonceWithFunction(nonce []byte, size uint32, function string) ([]byte return adjusted, nil } +func negotiateCharesResponse(accept *string) (charesResponse, error) { + defaultResponse := charesResponse{ + format: charesResponseLegacy, + contentType: legacyCharesResponseMediaType, + } + if accept == nil || strings.TrimSpace(*accept) == "" { + return defaultResponse, nil + } + + for _, offered := range splitAcceptHeader(*accept) { + offered = strings.TrimSpace(offered) + if offered == "*/*" { + return defaultResponse, nil + } + + mediaType, params, err := mime.ParseMediaType(offered) + if err != nil { + continue + } + + switch mediaType { + case "application/eat-ucs+json": + if params["eat_profile"] == ratsdtoken.LegacyProfile { + return defaultResponse, nil + } + case "application/cmw+cbor": + if params["cmwct"] == ratsdtokenv2.Profile { + return charesResponse{ + format: charesResponseV2, + contentType: v2CharesResponseMediaType, + }, nil + } + } + } + + return charesResponse{}, fmt.Errorf( + "wrong accept type, expect %s or %s (got %s)", + legacyCharesResponseMediaType, + v2CharesResponseMediaType, + *accept, + ) +} + +func splitAcceptHeader(accept string) []string { + values := []string{} + start := 0 + inQuotes := false + escaped := false + + for i, r := range accept { + switch { + case escaped: + escaped = false + case r == '\\' && inQuotes: + escaped = true + case r == '"': + inQuotes = !inQuotes + case r == ',' && !inQuotes: + values = append(values, accept[start:i]) + start = i + 1 + } + } + + return append(values, accept[start:]) +} + func NewServer(logger *zap.SugaredLogger, manager plugin.IManager, options string) *Server { return &Server{ logger: logger, @@ -103,21 +187,19 @@ func (s *Server) RatsdChares(w http.ResponseWriter, r *http.Request, param Ratsd return } - respCt := fmt.Sprintf(`application/eat-ucs+json; eat_profile=%q`, TagGithubCom2024Veraisonratsd) + resp, err := negotiateCharesResponse(param.Accept) if param.Accept != nil { s.logger.Info("request media type: ", *(param.Accept)) - if *(param.Accept) != respCt && *(param.Accept) != "*/*" { - errMsg := fmt.Sprintf( - "wrong accept type, expect %s (got %s)", respCt, *(param.Accept)) - p := problems.NewDetailedProblem(http.StatusNotAcceptable, errMsg) - s.reportProblem(w, p) - return - } + } + if err != nil { + p := problems.NewDetailedProblem(http.StatusNotAcceptable, err.Error()) + s.reportProblem(w, p) + return } payload, _ := io.ReadAll(r.Body) requestFields := make(map[string]json.RawMessage) - err := json.Unmarshal(payload, &requestFields) + err = json.Unmarshal(payload, &requestFields) if err != nil { errMsg := "unable to deserialize JSON request body" p := &problems.DefaultProblem{ @@ -201,10 +283,16 @@ func (s *Server) RatsdChares(w http.ResponseWriter, r *http.Request, param Ratsd return } s.logger.Info("request nonce: ", requestNonce) - s.logger.Info("request media type: ", *(param.Accept)) - - evidence := ratsdtoken.NewEvidence() - if err := evidence.Claims.SetNonce(nonce); err != nil { + s.logger.Info("response media type: ", resp.contentType) + + legacyEvidence := ratsdtoken.NewEvidence() + v2Evidence := ratsdtokenv2.NewEvidence() + if resp.format == charesResponseV2 { + err = v2Evidence.Claims.SetNonce(nonce) + } else { + err = legacyEvidence.Claims.SetNonce(nonce) + } + if err != nil { errMsg := fmt.Errorf("invalid nonce in the request: %w", err).Error() p := &problems.DefaultProblem{ Type: string(TagGithubCom2024VeraisonratsdErrorInvalidrequest), @@ -216,7 +304,10 @@ func (s *Server) RatsdChares(w http.ResponseWriter, r *http.Request, param Ratsd return } - collection := cmw.NewCollection("tag:github.com,2025:veraison/ratsd/cmw") + var collection *cmw.CMW + if resp.format == charesResponseLegacy { + collection = cmw.NewCollection(legacyCMWCollectionType) + } pl := s.manager.GetPluginList() if len(pl) == 0 { errMsg := "no sub-attester available" @@ -304,14 +395,24 @@ func (s *Server) RatsdChares(w http.ResponseWriter, r *http.Request, param Ratsd return false } - if err := evidence.Claims.SetNonceAdjustFn(nonceAdjustFunction); err != nil { + if resp.format == charesResponseV2 { + err = v2Evidence.Claims.SetNonceAdjustFn(nonceAdjustFunction) + } else { + err = legacyEvidence.Claims.SetNonceAdjustFn(nonceAdjustFunction) + } + if err != nil { errMsg := fmt.Sprintf("failed to set nonce adjustment function: %s", err.Error()) p := problems.NewDetailedProblem(http.StatusInternalServerError, errMsg) s.reportProblem(w, p) return false } - if err := evidence.Claims.SetKeyandNonceSz(pn, uint(selectedFormat.NonceSize)); err != nil { + if resp.format == charesResponseV2 { + err = v2Evidence.Claims.SetKeyandNonceSz(pn, uint(selectedFormat.NonceSize)) + } else { + err = legacyEvidence.Claims.SetKeyandNonceSz(pn, uint(selectedFormat.NonceSize)) + } + if err != nil { errMsg := fmt.Sprintf("failed to set nonce adjustment map: %s", err.Error()) p := problems.NewDetailedProblem(http.StatusInternalServerError, errMsg) s.reportProblem(w, p) @@ -333,8 +434,17 @@ func (s *Server) RatsdChares(w http.ResponseWriter, r *http.Request, param Ratsd return false } - c := cmw.NewMonad(in.ContentType, out.Evidence) - collection.AddCollectionItem(pn, c) + if resp.format == charesResponseV2 { + if err := v2Evidence.SetToken(pn, in.ContentType, out.Evidence, cmw.Evidence); err != nil { + errMsg := fmt.Sprintf("failed to add evidence from %s: %s", pn, err.Error()) + p := problems.NewDetailedProblem(http.StatusInternalServerError, errMsg) + s.reportProblem(w, p) + return false + } + } else { + c := cmw.NewMonad(in.ContentType, out.Evidence) + collection.AddCollectionItem(pn, c) + } return true } @@ -357,14 +467,27 @@ func (s *Server) RatsdChares(w http.ResponseWriter, r *http.Request, param Ratsd } } - if err := evidence.Claims.SetCMW(collection); err != nil { - errMsg := fmt.Sprintf("failed to serialize CMW collection: %s", err.Error()) - p := problems.NewDetailedProblem(http.StatusInternalServerError, errMsg) - s.reportProblem(w, p) - return - } + var response []byte + if resp.format == charesResponseV2 { + // Token signing is not configured by the API yet, but COSE_Sign1 + // serialization requires a non-empty signature field. + if err := v2Evidence.SetSignature([]byte{0}); err != nil { + errMsg := fmt.Sprintf("failed to set RATSD v2 token signature: %s", err.Error()) + p := problems.NewDetailedProblem(http.StatusInternalServerError, errMsg) + s.reportProblem(w, p) + return + } + response, err = v2Evidence.MarshalCBOR() + } else { + if err := legacyEvidence.Claims.SetCMW(collection); err != nil { + errMsg := fmt.Sprintf("failed to serialize CMW collection: %s", err.Error()) + p := problems.NewDetailedProblem(http.StatusInternalServerError, errMsg) + s.reportProblem(w, p) + return + } - response, err := evidence.MarshalJSON() + response, err = legacyEvidence.MarshalJSON() + } if err != nil { errMsg := fmt.Sprintf("failed to serialize RATSD token: %s", err.Error()) p := problems.NewDetailedProblem(http.StatusInternalServerError, errMsg) @@ -372,7 +495,7 @@ func (s *Server) RatsdChares(w http.ResponseWriter, r *http.Request, param Ratsd return } - w.Header().Set("Content-Type", respCt) + w.Header().Set("Content-Type", resp.contentType) w.WriteHeader(http.StatusOK) w.Write(response) } diff --git a/api/server_test.go b/api/server_test.go index 1821ce5..0d6466b 100644 --- a/api/server_test.go +++ b/api/server_test.go @@ -16,12 +16,14 @@ import ( "github.com/golang/mock/gomock" "github.com/moogar0880/problems" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/veraison/cmw" mock_deps "github.com/veraison/ratsd/api/mocks" "github.com/veraison/ratsd/attesters/mocktsm" "github.com/veraison/ratsd/attesters/tsm" "github.com/veraison/ratsd/proto/compositor" ratsdtoken "github.com/veraison/ratsd/ratsd-token" + ratsdtokenv2 "github.com/veraison/ratsd/ratsd-token-v2" "github.com/veraison/ratsd/tokens" "github.com/veraison/services/log" ) @@ -44,6 +46,21 @@ func decodeCharesClaims(t *testing.T, body []byte) ratsdtoken.Claims { return claims } +func decodeCharesV2(t *testing.T, body []byte) (ratsdtokenv2.Claims, cmw.CMW, []byte) { + t.Helper() + + var evidence ratsdtokenv2.Evidence + require.NoError(t, evidence.UnmarshalCBOR(body)) + + claims, err := evidence.GetClaims() + require.NoError(t, err) + + collection, err := evidence.GetCollection() + require.NoError(t, err) + + return claims, collection, evidence.GetSignature() +} + func adjustNonceForTest(t *testing.T, nonce []byte, size uint32) []byte { t.Helper() @@ -170,6 +187,33 @@ func TestRatsdChares_wrong_content_type(t *testing.T) { assert.Equal(t, expectedBody, &body) } +func TestRatsdChares_defaults_to_legacy_response_without_accept(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + logger := log.Named("test") + + pluginList := []string{"mock-tsm"} + dm := mock_deps.NewMockIManager(ctrl) + dm.EXPECT().GetPluginList().Return(pluginList).AnyTimes() + dm.EXPECT().LookupByName("mock-tsm").Return(mocktsm.GetPlugin(), nil).AnyTimes() + + s := NewServer(logger, dm, "all") + w := httptest.NewRecorder() + rb := strings.NewReader(fmt.Sprintf(`{"nonce": "%s"}`, validNonce)) + r, _ := http.NewRequest(http.MethodPost, "/ratsd/chares", rb) + r.Header.Add("Content-Type", ApplicationvndVeraisonCharesJson) + s.RatsdChares(w, r, RatsdCharesParams{}) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, legacyCharesResponseMediaType, w.Result().Header.Get("Content-Type")) + + claims := decodeCharesClaims(t, w.Body.Bytes()) + profile, err := claims.EatProfile.Get() + assert.NoError(t, err) + assert.Equal(t, ratsdtoken.LegacyProfile, profile) +} + func TestRatsdChares_wrong_accept_type(t *testing.T) { var params RatsdCharesParams @@ -185,7 +229,12 @@ func TestRatsdChares_wrong_accept_type(t *testing.T) { respCt := fmt.Sprintf(`application/eat-ucs+json; eat_profile=%q`, TagGithubCom2024Veraisonratsd) expectedCode := http.StatusNotAcceptable expectedType := problems.ProblemMediaType - expectedDetail := fmt.Sprintf("wrong accept type, expect %s (got %s)", respCt, *(params.Accept)) + expectedDetail := fmt.Sprintf( + "wrong accept type, expect %s or %s (got %s)", + respCt, + v2CharesResponseMediaType, + *(params.Accept), + ) expectedBody := problems.NewDetailedProblem(http.StatusNotAcceptable, expectedDetail) var body problems.DefaultProblem @@ -401,6 +450,69 @@ func TestRatsdChares_valid_request(t *testing.T) { } } +func TestRatsdChares_valid_request_v2(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + var params RatsdCharesParams + + param := v2CharesResponseMediaType + params.Accept = ¶m + logger := log.Named("test") + + pluginList := []string{"mock-tsm"} + dm := mock_deps.NewMockIManager(ctrl) + dm.EXPECT().GetPluginList().Return(pluginList).AnyTimes() + dm.EXPECT().LookupByName("mock-tsm").Return(mocktsm.GetPlugin(), nil).AnyTimes() + + s := NewServer(logger, dm, "all") + realNonce, _ := base64.RawURLEncoding.DecodeString(validNonce) + adjustedNonce := adjustNonceForTest(t, realNonce, 64) + + w := httptest.NewRecorder() + rb := strings.NewReader(fmt.Sprintf(`{"nonce": "%s", + "mock-tsm":{ + "privilege_level":"1" + } + }`, validNonce)) + r, _ := http.NewRequest(http.MethodPost, "/ratsd/chares", rb) + r.Header.Add("Content-Type", ApplicationvndVeraisonCharesJson) + s.RatsdChares(w, r, params) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, param, w.Result().Header.Get("Content-Type")) + + claims, collection, signature := decodeCharesV2(t, w.Body.Bytes()) + + assert.Equal(t, ratsdtokenv2.Profile, claims.GetEatProfile()) + assert.Equal(t, realNonce, claims.GetEatNonce()) + assert.Equal(t, ratsdtokenv2.DefaultLeadAttesterOEMID, claims.GetOEMID()) + assert.Equal(t, ratsdtokenv2.DefaultLeadAttesterSWName, claims.GetSWName()) + assert.Equal(t, ratsdtokenv2.DefaultLeadAttesterSWVersion, claims.GetSWVersion()) + assert.Equal(t, ratsdtokenv2.NonceAdjustFunctionShake256, claims.GetNonceAdjustFn()) + assert.Equal(t, map[string]uint{"mock-tsm": 64}, claims.GetNonceAdjustMap()) + assert.Equal(t, []byte{0}, signature) + + collectionType, err := collection.GetCollectionType() + require.NoError(t, err) + assert.Equal(t, ratsdtokenv2.CMWCollectionType, collectionType) + + c, err := collection.GetCollectionItem("mock-tsm") + require.NoError(t, err) + assert.Equal(t, cmw.KindMonad, c.GetKind()) + assert.Equal(t, tokens.TSMReportMediaTypeJSON, c.GetMonadType()) + assert.Equal(t, cmw.Indicator(cmw.Evidence), c.GetMonadIndicator()) + + tsmout := &tokens.TSMReport{} + tsmout.FromJSON(c.GetMonadValue()) + assert.Equal(t, "fake\n", tsmout.Provider) + assert.Equal(t, tokens.BinaryString("auxblob"), tsmout.AuxBlob) + + expectedOutblob := fmt.Sprintf("privlevel: %d\ninblob: %s", 1, + hex.EncodeToString(adjustedNonce)) + assert.Equal(t, tokens.BinaryString(expectedOutblob), tsmout.OutBlob) +} + func TestRatsdChares_adjustsNonceToSelectedFormatSize(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() diff --git a/docs/api/ratsd.yaml b/docs/api/ratsd.yaml index 1c2f902..e98cae7 100644 --- a/docs/api/ratsd.yaml +++ b/docs/api/ratsd.yaml @@ -14,9 +14,13 @@ paths: '200': description: The request has succeeded. content: - application/eat+jwt; eat_profile="tag:github.com,2024:veraison/ratsd": + application/eat-ucs+json; eat_profile="tag:github.com,2024:veraison/ratsd": schema: $ref: '#/components/schemas/EAT' + application/cmw+cbor; cmwct="tag:github.com,2026:veraison/ratsd/v2": + schema: + type: string + format: binary '400': description: The server could not understand the request due to invalid syntax. content: