package server import ( "bytes" "context" "encoding/json" "fmt" "io/ioutil" "net/http" "strconv" "github.com/keys-pub/keys" "github.com/keys-pub/keys-ext/http/api" "github.com/keys-pub/keys/dstore" "github.com/keys-pub/keys/tsutil" "github.com/labstack/echo/v4" "github.com/pkg/errors" ) func (s *Server) sigchain(c echo.Context, kid keys.ID) (*keys.Sigchain, map[string]api.Metadata, error) { ctx := c.Request().Context() iter, err := s.fi.DocumentIterator(ctx, "sigchain", dstore.Prefix(kid.String())) if err != nil { return nil, nil, err } defer iter.Release() sc := keys.NewSigchain(kid) md := make(map[string]api.Metadata, 100) for { doc, err := iter.Next() if err != nil { return nil, nil, err } if doc == nil { break } var st *keys.Statement if err := json.Unmarshal(doc.Data(), &st); err != nil { return nil, nil, err } if err := sc.Add(st); err != nil { return nil, nil, err } md[st.URL()] = api.Metadata{ CreatedAt: doc.CreatedAt, UpdatedAt: doc.UpdatedAt, } } return sc, md, nil } func (s *Server) getSigchain(c echo.Context) error { s.logger.Infof("Server %s %s", c.Request().Method, c.Request().URL.String()) kid, err := keys.ParseID(c.Param("kid")) if err != nil { return s.ErrNotFound(c, nil) } s.logger.Infof("Loading sigchain: %s", kid) sc, md, err := s.sigchain(c, kid) if err != nil { return s.ErrInternalServer(c, err) } if sc.Length() == 0 { return s.ErrNotFound(c, errors.Errorf("sigchain not found")) } resp := api.SigchainResponse{ KID: kid, Statements: sc.Statements(), } fields := dstore.NewStringSetSplit(c.QueryParam("include"), ",") if fields.Contains("md") { resp.Metadata = md } return JSON(c, http.StatusOK, resp) } func (s *Server) getSigchainStatement(c echo.Context) error { s.logger.Infof("Server %s %s", c.Request().Method, c.Request().URL.String()) ctx := c.Request().Context() kid, err := keys.ParseID(c.Param("kid")) if err != nil { return s.ErrNotFound(c, err) } i, err := strconv.Atoi(c.Param("seq")) if err != nil { return s.ErrNotFound(c, err) } path := dstore.Path("sigchain", kid.WithSeq(i)) st, doc, err := s.statement(ctx, path) if st == nil { return s.ErrNotFound(c, errors.Errorf("statement not found")) } if err != nil { return s.ErrInternalServer(c, err) } if !doc.CreatedAt.IsZero() { c.Response().Header().Set("CreatedAt", doc.CreatedAt.Format(http.TimeFormat)) c.Response().Header().Set("CreatedAt-RFC3339M", doc.CreatedAt.Format(tsutil.RFC3339Milli)) } if !doc.UpdatedAt.IsZero() { c.Response().Header().Set("Last-Modified", doc.UpdatedAt.Format(http.TimeFormat)) c.Response().Header().Set("Last-Modified-RFC3339M", doc.UpdatedAt.Format(tsutil.RFC3339Milli)) } return JSON(c, http.StatusOK, st) } func (s *Server) putSigchainStatement(c echo.Context) error { s.logger.Infof("Server %s %s", c.Request().Method, c.Request().URL.String()) ctx := c.Request().Context() if c.Request().Body == nil { return s.ErrBadRequest(c, errors.Errorf("missing body")) } b, err := ioutil.ReadAll(c.Request().Body) if err != nil { return s.ErrInternalServer(c, err) } st, err := s.statementFromBytes(ctx, b) if err != nil { return s.ErrBadRequest(c, err) } if len(st.Data) > 16*1024 { return s.ErrBadRequest(c, errors.Errorf("too much data for sigchain statement (greater than 16KiB)")) } if c.Param("kid") != st.KID.String() { return s.ErrBadRequest(c, errors.Errorf("invalid kid")) } if c.Param("seq") != fmt.Sprintf("%d", st.Seq) { return s.ErrBadRequest(c, errors.Errorf("invalid seq")) } if st.Seq <= 0 { return s.ErrBadRequest(c, errors.Errorf("invalid seq")) } path := dstore.Path("sigchain", keys.StatementID(st.KID, st.Seq)) exists, err := s.fi.Exists(ctx, path) if err != nil { return s.ErrInternalServer(c, err) } if exists { return s.ErrConflict(c, errors.Errorf("statement already exists")) } sc, _, err := s.sigchain(c, st.KID) if err != nil { return s.ErrInternalServer(c, err) } if sc.Length() >= 1024 { // TODO: Increase limits return s.ErrEntityTooLarge(c, errors.Errorf("sigchain limit reached, contact gabriel@github to bump the limits")) } prev := sc.Last() if err := sc.VerifyStatement(st, prev); err != nil { return s.ErrBadRequest(c, err) } if err := sc.Add(st); err != nil { return s.ErrBadRequest(c, err) } // Check we don't have an existing user with a different key, which would cause duplicates in search. // They should revoke the existing user before linking a new key. // Since there is a delay in indexing this won't stop a malicious user from creating duplicates but // it will limit them. If we find spaming this is a problem, we can get more strict. existing, err := s.users.CheckForExisting(ctx, sc) if err != nil { return s.ErrInternalServer(c, err) } if existing != "" { if err := s.checkKID(ctx, existing, HighPriority); err != nil { return s.ErrInternalServer(c, err) } return s.ErrResponse(c, http.StatusConflict, errors.Errorf("user already exists with key %s, if you removed or revoked the previous statement you may need to wait briefly for search to update", existing)) } s.logger.Infof("Statement, set %s", path) if err := s.fi.Create(ctx, path, dstore.Data(b)); err != nil { return s.ErrInternalServer(c, err) } if err := s.sigchains.Index(st.KID); err != nil { return s.ErrInternalServer(c, err) } if err := s.checkKID(ctx, st.KID, HighPriority); err != nil { return s.ErrInternalServer(c, err) } var resp struct{} return JSON(c, http.StatusOK, resp) } func (s *Server) statement(ctx context.Context, path string) (*keys.Statement, *dstore.Document, error) { e, err := s.fi.Get(ctx, path) if err != nil { return nil, nil, err } if e == nil { return nil, nil, nil } st, err := s.statementFromBytes(ctx, e.Data()) if err != nil { return nil, nil, err } return st, e, nil } func (s *Server) statementFromBytes(ctx context.Context, b []byte) (*keys.Statement, error) { var st *keys.Statement if err := json.Unmarshal(b, &st); err != nil { return nil, err } bout, err := st.Bytes() if err != nil { return nil, err } if !bytes.Equal(b, bout) { s.logger.Errorf("%s != %s", string(b), string(bout)) return nil, errors.Errorf("invalid statement bytes") } if err := st.Verify(); err != nil { return st, err } return st, nil } func (s *Server) getSigchainAliased(c echo.Context) error { if c.Request().Host == "sigcha.in" { return s.getSigchain(c) } return s.ErrNotFound(c, nil) } func (s *Server) getSigchainStatementAliased(c echo.Context) error { if c.Request().Host == "sigcha.in" { return s.getSigchainStatement(c) } return s.ErrNotFound(c, nil) } func (s *Server) putSigchainStatementAliased(c echo.Context) error { // An earlier <= 0.0.48 just used PUT /:kid/:seq instead of /sigchain/:kid/:seq // for all hosts (including keys.pub), so we need to continue to support // that for awhile so we don't break those clients. // if c.Request().Host == "sigcha.in" { // return s.putSigchainStatement(c) // } // return s.ErrNotFound(c, nil) return s.putSigchainStatement(c) }