Skip to content

Commit f3d6147

Browse files
claudehulto
authored andcommitted
Add JWT-based task authentication using ed25519 keys
This commit implements JWT-based authentication for gRPC APIs: - Added JWT service using persistent ed25519 keys for signing - Updated protobufs to include JWT fields in: - Task (returned from ClaimTasks) - ReportTaskOutputRequest - FetchAssetRequest - ReportCredentialRequest - ReportFileRequest - Updated ClaimTasks to generate JWTs for each claimed task - Added JWT validation to gRPC APIs: - ReportTaskOutput - FetchAsset - ReportCredential - ReportFile - Invalid JWTs are logged with task ID and JWT token as warnings The ed25519 keys are stored persistently in the secrets manager, separate from the x25519 keys used for encryption.
1 parent 2a86d51 commit f3d6147

File tree

14 files changed

+571
-339
lines changed

14 files changed

+571
-339
lines changed

implants/lib/pb/src/generated/c2.rs

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tavern/app.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"realm.pub/tavern/internal/graphql"
3535
tavernhttp "realm.pub/tavern/internal/http"
3636
"realm.pub/tavern/internal/http/stream"
37+
"realm.pub/tavern/internal/jwt"
3738
"realm.pub/tavern/internal/portals"
3839
"realm.pub/tavern/internal/portals/mux"
3940
"realm.pub/tavern/internal/redirectors"
@@ -530,7 +531,14 @@ func newGRPCHandler(client *ent.Client, grpcShellMux *stream.Mux, portalMux *mux
530531
}
531532
slog.Info(fmt.Sprintf("public key: %s", base64.StdEncoding.EncodeToString(pub.Bytes())))
532533

533-
c2srv := c2.New(client, grpcShellMux, portalMux)
534+
// Initialize JWT service
535+
jwtService, err := jwt.NewService()
536+
if err != nil {
537+
slog.Error("failed to initialize JWT service", "err", err)
538+
panic(err)
539+
}
540+
541+
c2srv := c2.New(client, grpcShellMux, portalMux, jwtService)
534542
xchacha := cryptocodec.StreamDecryptCodec{
535543
Csvc: cryptocodec.NewCryptoSvc(priv),
536544
}

tavern/internal/c2/api_claim_tasks.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,9 +400,19 @@ func (srv *Server) ClaimTasks(ctx context.Context, req *c2pb.ClaimTasksRequest)
400400
for _, a := range claimedAssets {
401401
claimedAssetNames = append(claimedAssetNames, a.Name)
402402
}
403+
404+
// Generate JWT for this task
405+
taskJWT, err := srv.jwtService.GenerateTaskToken(int64(claimedTask.ID), req.Beacon.Identifier)
406+
if err != nil {
407+
slog.ErrorContext(ctx, "failed to generate JWT for task", "task_id", claimedTask.ID, "err", err)
408+
// Continue without JWT - this is not critical for task execution
409+
taskJWT = ""
410+
}
411+
403412
resp.Tasks = append(resp.Tasks, &c2pb.Task{
404413
Id: int64(claimedTask.ID),
405414
QuestName: claimedQuest.Name,
415+
Jwt: taskJWT,
406416
Tome: &epb.Tome{
407417
Eldritch: claimedTome.Eldritch,
408418
Parameters: params,

tavern/internal/c2/api_fetch_asset.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ import (
1515
func (srv *Server) FetchAsset(req *c2pb.FetchAssetRequest, stream c2pb.C2_FetchAssetServer) error {
1616
ctx := stream.Context()
1717

18+
// Validate JWT
19+
taskID := srv.validateJWT(ctx, req.Jwt)
20+
if taskID == -1 {
21+
// JWT validation failed, but we'll continue (warning already logged)
22+
// Note: task_id is not used for asset fetching, but we validate the JWT anyway
23+
}
24+
1825
// Load Asset
1926
name := req.GetName()
2027
a, err := srv.graph.Asset.Query().

tavern/internal/c2/api_report_credential.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ func (srv *Server) ReportCredential(ctx context.Context, req *c2pb.ReportCredent
1818
return nil, status.Errorf(codes.InvalidArgument, "must provide credential")
1919
}
2020

21+
// Validate JWT
22+
srv.validateTaskJWT(ctx, req.Jwt, req.TaskId)
23+
2124
// Load Task
2225
task, err := srv.graph.Task.Get(ctx, int(req.TaskId))
2326
if ent.IsNotFound(err) {

tavern/internal/c2/api_report_file.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ func (srv *Server) ReportFile(stream c2pb.C2_ReportFileServer) error {
2121
permissions string
2222
size uint64
2323
hash string
24+
jwtToken string
2425

2526
content []byte
2627
)
@@ -42,6 +43,9 @@ func (srv *Server) ReportFile(stream c2pb.C2_ReportFileServer) error {
4243
if taskID == 0 {
4344
taskID = req.GetTaskId()
4445
}
46+
if jwtToken == "" {
47+
jwtToken = req.GetJwt()
48+
}
4549
if path == "" && req.Chunk.Metadata != nil {
4650
path = req.Chunk.Metadata.GetPath()
4751
}
@@ -71,6 +75,9 @@ func (srv *Server) ReportFile(stream c2pb.C2_ReportFileServer) error {
7175
return status.Errorf(codes.InvalidArgument, "must provide valid path")
7276
}
7377

78+
// Validate JWT
79+
srv.validateTaskJWT(stream.Context(), jwtToken, taskID)
80+
7481
// Load Task
7582
task, err := srv.graph.Task.Get(stream.Context(), int(taskID))
7683
if ent.IsNotFound(err) {

tavern/internal/c2/api_report_task_output.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ func (srv *Server) ReportTaskOutput(ctx context.Context, req *c2pb.ReportTaskOut
1717
return nil, status.Errorf(codes.InvalidArgument, "must provide task id")
1818
}
1919

20+
// Validate JWT
21+
srv.validateTaskJWT(ctx, req.Jwt, req.Output.Id)
22+
2023
// Parse Input
2124
var (
2225
execStartedAt *time.Time

tavern/internal/c2/c2pb/c2.pb.go

Lines changed: 153 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)