Implement transit encryption engine with versioned key management
Add complete transit engine supporting symmetric encryption (AES-256-GCM, XChaCha20-Poly1305), asymmetric signing (Ed25519, ECDSA P-256/P-384), and HMAC (SHA-256/SHA-512) with versioned key rotation, min decryption version enforcement, key trimming, batch operations, and rewrap. Includes proto definitions, gRPC handlers, REST routes, and comprehensive tests covering all 18 operations, auth enforcement, and edge cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1602
internal/engine/transit/transit.go
Normal file
1602
internal/engine/transit/transit.go
Normal file
File diff suppressed because it is too large
Load Diff
1025
internal/engine/transit/transit_test.go
Normal file
1025
internal/engine/transit/transit_test.go
Normal file
File diff suppressed because it is too large
Load Diff
15
internal/engine/transit/types.go
Normal file
15
internal/engine/transit/types.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package transit
|
||||
|
||||
// TransitConfig is the transit engine configuration stored in the barrier.
|
||||
type TransitConfig struct {
|
||||
MaxKeyVersions int `json:"max_key_versions"`
|
||||
}
|
||||
|
||||
// KeyConfig is per-key configuration stored in the barrier.
|
||||
type KeyConfig struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // aes256-gcm, chacha20-poly, ed25519, ecdsa-p256, ecdsa-p384, hmac-sha256, hmac-sha512
|
||||
CurrentVersion int `json:"current_version"`
|
||||
MinDecryptionVersion int `json:"min_decryption_version"`
|
||||
AllowDeletion bool `json:"allow_deletion"`
|
||||
}
|
||||
@@ -82,6 +82,7 @@ func (s *GRPCServer) Start() error {
|
||||
pb.RegisterCAServiceServer(s.srv, &caServer{s: s})
|
||||
pb.RegisterPolicyServiceServer(s.srv, &policyServer{s: s})
|
||||
pb.RegisterACMEServiceServer(s.srv, &acmeServer{s: s})
|
||||
pb.RegisterTransitServiceServer(s.srv, &transitServer{s: s})
|
||||
|
||||
lis, err := net.Listen("tcp", s.cfg.Server.GRPCAddr)
|
||||
if err != nil {
|
||||
@@ -136,7 +137,24 @@ func sealRequiredMethods() map[string]bool {
|
||||
"/metacrypt.v2.ACMEService/CreateEAB": true,
|
||||
"/metacrypt.v2.ACMEService/SetConfig": true,
|
||||
"/metacrypt.v2.ACMEService/ListAccounts": true,
|
||||
"/metacrypt.v2.ACMEService/ListOrders": true,
|
||||
"/metacrypt.v2.ACMEService/ListOrders": true,
|
||||
"/metacrypt.v2.TransitService/CreateKey": true,
|
||||
"/metacrypt.v2.TransitService/DeleteKey": true,
|
||||
"/metacrypt.v2.TransitService/GetKey": true,
|
||||
"/metacrypt.v2.TransitService/ListKeys": true,
|
||||
"/metacrypt.v2.TransitService/RotateKey": true,
|
||||
"/metacrypt.v2.TransitService/UpdateKeyConfig": true,
|
||||
"/metacrypt.v2.TransitService/TrimKey": true,
|
||||
"/metacrypt.v2.TransitService/Encrypt": true,
|
||||
"/metacrypt.v2.TransitService/Decrypt": true,
|
||||
"/metacrypt.v2.TransitService/Rewrap": true,
|
||||
"/metacrypt.v2.TransitService/BatchEncrypt": true,
|
||||
"/metacrypt.v2.TransitService/BatchDecrypt": true,
|
||||
"/metacrypt.v2.TransitService/BatchRewrap": true,
|
||||
"/metacrypt.v2.TransitService/Sign": true,
|
||||
"/metacrypt.v2.TransitService/Verify": true,
|
||||
"/metacrypt.v2.TransitService/Hmac": true,
|
||||
"/metacrypt.v2.TransitService/GetPublicKey": true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +184,24 @@ func authRequiredMethods() map[string]bool {
|
||||
"/metacrypt.v2.ACMEService/CreateEAB": true,
|
||||
"/metacrypt.v2.ACMEService/SetConfig": true,
|
||||
"/metacrypt.v2.ACMEService/ListAccounts": true,
|
||||
"/metacrypt.v2.ACMEService/ListOrders": true,
|
||||
"/metacrypt.v2.ACMEService/ListOrders": true,
|
||||
"/metacrypt.v2.TransitService/CreateKey": true,
|
||||
"/metacrypt.v2.TransitService/DeleteKey": true,
|
||||
"/metacrypt.v2.TransitService/GetKey": true,
|
||||
"/metacrypt.v2.TransitService/ListKeys": true,
|
||||
"/metacrypt.v2.TransitService/RotateKey": true,
|
||||
"/metacrypt.v2.TransitService/UpdateKeyConfig": true,
|
||||
"/metacrypt.v2.TransitService/TrimKey": true,
|
||||
"/metacrypt.v2.TransitService/Encrypt": true,
|
||||
"/metacrypt.v2.TransitService/Decrypt": true,
|
||||
"/metacrypt.v2.TransitService/Rewrap": true,
|
||||
"/metacrypt.v2.TransitService/BatchEncrypt": true,
|
||||
"/metacrypt.v2.TransitService/BatchDecrypt": true,
|
||||
"/metacrypt.v2.TransitService/BatchRewrap": true,
|
||||
"/metacrypt.v2.TransitService/Sign": true,
|
||||
"/metacrypt.v2.TransitService/Verify": true,
|
||||
"/metacrypt.v2.TransitService/Hmac": true,
|
||||
"/metacrypt.v2.TransitService/GetPublicKey": true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +220,11 @@ func adminRequiredMethods() map[string]bool {
|
||||
"/metacrypt.v2.PolicyService/DeletePolicy": true,
|
||||
"/metacrypt.v2.ACMEService/SetConfig": true,
|
||||
"/metacrypt.v2.ACMEService/ListAccounts": true,
|
||||
"/metacrypt.v2.ACMEService/ListOrders": true,
|
||||
"/metacrypt.v2.ACMEService/ListOrders": true,
|
||||
"/metacrypt.v2.TransitService/CreateKey": true,
|
||||
"/metacrypt.v2.TransitService/DeleteKey": true,
|
||||
"/metacrypt.v2.TransitService/RotateKey": true,
|
||||
"/metacrypt.v2.TransitService/UpdateKeyConfig": true,
|
||||
"/metacrypt.v2.TransitService/TrimKey": true,
|
||||
}
|
||||
}
|
||||
|
||||
486
internal/grpcserver/transit.go
Normal file
486
internal/grpcserver/transit.go
Normal file
@@ -0,0 +1,486 @@
|
||||
package grpcserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
pb "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v2"
|
||||
"git.wntrmute.dev/kyle/metacrypt/internal/engine"
|
||||
"git.wntrmute.dev/kyle/metacrypt/internal/engine/transit"
|
||||
"git.wntrmute.dev/kyle/metacrypt/internal/policy"
|
||||
)
|
||||
|
||||
type transitServer struct {
|
||||
pb.UnimplementedTransitServiceServer
|
||||
s *GRPCServer
|
||||
}
|
||||
|
||||
func (ts *transitServer) transitHandleRequest(ctx context.Context, mount, operation string, req *engine.Request) (*engine.Response, error) {
|
||||
resp, err := ts.s.engines.HandleRequest(ctx, mount, req)
|
||||
if err != nil {
|
||||
st := codes.Internal
|
||||
switch {
|
||||
case errors.Is(err, engine.ErrMountNotFound):
|
||||
st = codes.NotFound
|
||||
case errors.Is(err, transit.ErrKeyNotFound):
|
||||
st = codes.NotFound
|
||||
case errors.Is(err, transit.ErrKeyExists):
|
||||
st = codes.AlreadyExists
|
||||
case errors.Is(err, transit.ErrUnauthorized):
|
||||
st = codes.Unauthenticated
|
||||
case errors.Is(err, transit.ErrForbidden):
|
||||
st = codes.PermissionDenied
|
||||
case errors.Is(err, transit.ErrDeletionDenied):
|
||||
st = codes.FailedPrecondition
|
||||
case errors.Is(err, transit.ErrUnsupportedOp):
|
||||
st = codes.InvalidArgument
|
||||
case errors.Is(err, transit.ErrDecryptVersion):
|
||||
st = codes.FailedPrecondition
|
||||
case errors.Is(err, transit.ErrInvalidFormat):
|
||||
st = codes.InvalidArgument
|
||||
case errors.Is(err, transit.ErrBatchTooLarge):
|
||||
st = codes.InvalidArgument
|
||||
case errors.Is(err, transit.ErrInvalidMinVer):
|
||||
st = codes.InvalidArgument
|
||||
case strings.Contains(err.Error(), "not found"):
|
||||
st = codes.NotFound
|
||||
case strings.Contains(err.Error(), "forbidden"):
|
||||
st = codes.PermissionDenied
|
||||
}
|
||||
ts.s.logger.Error("grpc: transit "+operation, "mount", mount, "error", err)
|
||||
return nil, status.Error(st, err.Error())
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (ts *transitServer) callerInfo(ctx context.Context) *engine.CallerInfo {
|
||||
ti := tokenInfoFromContext(ctx)
|
||||
if ti == nil {
|
||||
return nil
|
||||
}
|
||||
return &engine.CallerInfo{
|
||||
Username: ti.Username,
|
||||
Roles: ti.Roles,
|
||||
IsAdmin: ti.IsAdmin,
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *transitServer) policyChecker(ctx context.Context) engine.PolicyChecker {
|
||||
caller := ts.callerInfo(ctx)
|
||||
if caller == nil {
|
||||
return nil
|
||||
}
|
||||
return func(resource, action string) (string, bool) {
|
||||
pReq := &policy.Request{
|
||||
Username: caller.Username,
|
||||
Roles: caller.Roles,
|
||||
Resource: resource,
|
||||
Action: action,
|
||||
}
|
||||
effect, matched, err := ts.s.policy.Match(ctx, pReq)
|
||||
if err != nil {
|
||||
return string(policy.EffectDeny), false
|
||||
}
|
||||
return string(effect), matched
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *transitServer) CreateKey(ctx context.Context, req *pb.CreateTransitKeyRequest) (*pb.CreateTransitKeyResponse, error) {
|
||||
if req.Mount == "" || req.Name == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount and name are required")
|
||||
}
|
||||
resp, err := ts.transitHandleRequest(ctx, req.Mount, "create-key", &engine.Request{
|
||||
Operation: "create-key",
|
||||
CallerInfo: ts.callerInfo(ctx),
|
||||
Data: map[string]interface{}{
|
||||
"name": req.Name,
|
||||
"type": req.Type,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name, _ := resp.Data["name"].(string)
|
||||
keyType, _ := resp.Data["type"].(string)
|
||||
version, _ := resp.Data["version"].(int)
|
||||
ts.s.logger.Info("audit: transit key created", "mount", req.Mount, "key", name, "type", keyType, "username", callerUsername(ctx))
|
||||
return &pb.CreateTransitKeyResponse{Name: name, Type: keyType, Version: int32(version)}, nil
|
||||
}
|
||||
|
||||
func (ts *transitServer) DeleteKey(ctx context.Context, req *pb.DeleteTransitKeyRequest) (*pb.DeleteTransitKeyResponse, error) {
|
||||
if req.Mount == "" || req.Name == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount and name are required")
|
||||
}
|
||||
_, err := ts.transitHandleRequest(ctx, req.Mount, "delete-key", &engine.Request{
|
||||
Operation: "delete-key",
|
||||
CallerInfo: ts.callerInfo(ctx),
|
||||
Data: map[string]interface{}{"name": req.Name},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ts.s.logger.Info("audit: transit key deleted", "mount", req.Mount, "key", req.Name, "username", callerUsername(ctx))
|
||||
return &pb.DeleteTransitKeyResponse{}, nil
|
||||
}
|
||||
|
||||
func (ts *transitServer) GetKey(ctx context.Context, req *pb.GetTransitKeyRequest) (*pb.GetTransitKeyResponse, error) {
|
||||
if req.Mount == "" || req.Name == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount and name are required")
|
||||
}
|
||||
resp, err := ts.transitHandleRequest(ctx, req.Mount, "get-key", &engine.Request{
|
||||
Operation: "get-key",
|
||||
CallerInfo: ts.callerInfo(ctx),
|
||||
Data: map[string]interface{}{"name": req.Name},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name, _ := resp.Data["name"].(string)
|
||||
keyType, _ := resp.Data["type"].(string)
|
||||
currentVersion, _ := resp.Data["current_version"].(int)
|
||||
minDecryptionVersion, _ := resp.Data["min_decryption_version"].(int)
|
||||
allowDeletion, _ := resp.Data["allow_deletion"].(bool)
|
||||
rawVersions, _ := resp.Data["versions"].([]int)
|
||||
versions := make([]int32, len(rawVersions))
|
||||
for i, v := range rawVersions {
|
||||
versions[i] = int32(v)
|
||||
}
|
||||
return &pb.GetTransitKeyResponse{
|
||||
Name: name,
|
||||
Type: keyType,
|
||||
CurrentVersion: int32(currentVersion),
|
||||
MinDecryptionVersion: int32(minDecryptionVersion),
|
||||
AllowDeletion: allowDeletion,
|
||||
Versions: versions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ts *transitServer) ListKeys(ctx context.Context, req *pb.ListTransitKeysRequest) (*pb.ListTransitKeysResponse, error) {
|
||||
if req.Mount == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount is required")
|
||||
}
|
||||
resp, err := ts.transitHandleRequest(ctx, req.Mount, "list-keys", &engine.Request{
|
||||
Operation: "list-keys",
|
||||
CallerInfo: ts.callerInfo(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys := toStringSliceFromInterface(resp.Data["keys"])
|
||||
return &pb.ListTransitKeysResponse{Keys: keys}, nil
|
||||
}
|
||||
|
||||
func (ts *transitServer) RotateKey(ctx context.Context, req *pb.RotateTransitKeyRequest) (*pb.RotateTransitKeyResponse, error) {
|
||||
if req.Mount == "" || req.Name == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount and name are required")
|
||||
}
|
||||
resp, err := ts.transitHandleRequest(ctx, req.Mount, "rotate-key", &engine.Request{
|
||||
Operation: "rotate-key",
|
||||
CallerInfo: ts.callerInfo(ctx),
|
||||
Data: map[string]interface{}{"name": req.Name},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name, _ := resp.Data["name"].(string)
|
||||
version, _ := resp.Data["version"].(int)
|
||||
ts.s.logger.Info("audit: transit key rotated", "mount", req.Mount, "key", name, "version", version, "username", callerUsername(ctx))
|
||||
return &pb.RotateTransitKeyResponse{Name: name, Version: int32(version)}, nil
|
||||
}
|
||||
|
||||
func (ts *transitServer) UpdateKeyConfig(ctx context.Context, req *pb.UpdateTransitKeyConfigRequest) (*pb.UpdateTransitKeyConfigResponse, error) {
|
||||
if req.Mount == "" || req.Name == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount and name are required")
|
||||
}
|
||||
data := map[string]interface{}{"name": req.Name}
|
||||
if req.MinDecryptionVersion != 0 {
|
||||
data["min_decryption_version"] = float64(req.MinDecryptionVersion)
|
||||
}
|
||||
data["allow_deletion"] = req.AllowDeletion
|
||||
|
||||
_, err := ts.transitHandleRequest(ctx, req.Mount, "update-key-config", &engine.Request{
|
||||
Operation: "update-key-config",
|
||||
CallerInfo: ts.callerInfo(ctx),
|
||||
Data: data,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.UpdateTransitKeyConfigResponse{}, nil
|
||||
}
|
||||
|
||||
func (ts *transitServer) TrimKey(ctx context.Context, req *pb.TrimTransitKeyRequest) (*pb.TrimTransitKeyResponse, error) {
|
||||
if req.Mount == "" || req.Name == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount and name are required")
|
||||
}
|
||||
resp, err := ts.transitHandleRequest(ctx, req.Mount, "trim-key", &engine.Request{
|
||||
Operation: "trim-key",
|
||||
CallerInfo: ts.callerInfo(ctx),
|
||||
Data: map[string]interface{}{"name": req.Name},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
trimmed, _ := resp.Data["trimmed"].(int)
|
||||
return &pb.TrimTransitKeyResponse{Trimmed: int32(trimmed)}, nil
|
||||
}
|
||||
|
||||
func (ts *transitServer) Encrypt(ctx context.Context, req *pb.TransitEncryptRequest) (*pb.TransitEncryptResponse, error) {
|
||||
if req.Mount == "" || req.Key == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount and key are required")
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"key": req.Key,
|
||||
"plaintext": req.Plaintext,
|
||||
}
|
||||
if req.Context != "" {
|
||||
data["context"] = req.Context
|
||||
}
|
||||
resp, err := ts.transitHandleRequest(ctx, req.Mount, "encrypt", &engine.Request{
|
||||
Operation: "encrypt",
|
||||
CallerInfo: ts.callerInfo(ctx),
|
||||
CheckPolicy: ts.policyChecker(ctx),
|
||||
Data: data,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ct, _ := resp.Data["ciphertext"].(string)
|
||||
return &pb.TransitEncryptResponse{Ciphertext: ct}, nil
|
||||
}
|
||||
|
||||
func (ts *transitServer) Decrypt(ctx context.Context, req *pb.TransitDecryptRequest) (*pb.TransitDecryptResponse, error) {
|
||||
if req.Mount == "" || req.Key == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount and key are required")
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"key": req.Key,
|
||||
"ciphertext": req.Ciphertext,
|
||||
}
|
||||
if req.Context != "" {
|
||||
data["context"] = req.Context
|
||||
}
|
||||
resp, err := ts.transitHandleRequest(ctx, req.Mount, "decrypt", &engine.Request{
|
||||
Operation: "decrypt",
|
||||
CallerInfo: ts.callerInfo(ctx),
|
||||
CheckPolicy: ts.policyChecker(ctx),
|
||||
Data: data,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pt, _ := resp.Data["plaintext"].(string)
|
||||
return &pb.TransitDecryptResponse{Plaintext: pt}, nil
|
||||
}
|
||||
|
||||
func (ts *transitServer) Rewrap(ctx context.Context, req *pb.TransitRewrapRequest) (*pb.TransitRewrapResponse, error) {
|
||||
if req.Mount == "" || req.Key == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount and key are required")
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"key": req.Key,
|
||||
"ciphertext": req.Ciphertext,
|
||||
}
|
||||
if req.Context != "" {
|
||||
data["context"] = req.Context
|
||||
}
|
||||
resp, err := ts.transitHandleRequest(ctx, req.Mount, "rewrap", &engine.Request{
|
||||
Operation: "rewrap",
|
||||
CallerInfo: ts.callerInfo(ctx),
|
||||
CheckPolicy: ts.policyChecker(ctx),
|
||||
Data: data,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ct, _ := resp.Data["ciphertext"].(string)
|
||||
return &pb.TransitRewrapResponse{Ciphertext: ct}, nil
|
||||
}
|
||||
|
||||
func (ts *transitServer) BatchEncrypt(ctx context.Context, req *pb.TransitBatchEncryptRequest) (*pb.TransitBatchResponse, error) {
|
||||
if req.Mount == "" || req.Key == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount and key are required")
|
||||
}
|
||||
items := protoItemsToInterface(req.Items)
|
||||
resp, err := ts.transitHandleRequest(ctx, req.Mount, "batch-encrypt", &engine.Request{
|
||||
Operation: "batch-encrypt",
|
||||
CallerInfo: ts.callerInfo(ctx),
|
||||
CheckPolicy: ts.policyChecker(ctx),
|
||||
Data: map[string]interface{}{"key": req.Key, "items": items},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toBatchResponse(resp), nil
|
||||
}
|
||||
|
||||
func (ts *transitServer) BatchDecrypt(ctx context.Context, req *pb.TransitBatchDecryptRequest) (*pb.TransitBatchResponse, error) {
|
||||
if req.Mount == "" || req.Key == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount and key are required")
|
||||
}
|
||||
items := protoItemsToInterface(req.Items)
|
||||
resp, err := ts.transitHandleRequest(ctx, req.Mount, "batch-decrypt", &engine.Request{
|
||||
Operation: "batch-decrypt",
|
||||
CallerInfo: ts.callerInfo(ctx),
|
||||
CheckPolicy: ts.policyChecker(ctx),
|
||||
Data: map[string]interface{}{"key": req.Key, "items": items},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toBatchResponse(resp), nil
|
||||
}
|
||||
|
||||
func (ts *transitServer) BatchRewrap(ctx context.Context, req *pb.TransitBatchRewrapRequest) (*pb.TransitBatchResponse, error) {
|
||||
if req.Mount == "" || req.Key == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount and key are required")
|
||||
}
|
||||
items := protoItemsToInterface(req.Items)
|
||||
resp, err := ts.transitHandleRequest(ctx, req.Mount, "batch-rewrap", &engine.Request{
|
||||
Operation: "batch-rewrap",
|
||||
CallerInfo: ts.callerInfo(ctx),
|
||||
CheckPolicy: ts.policyChecker(ctx),
|
||||
Data: map[string]interface{}{"key": req.Key, "items": items},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toBatchResponse(resp), nil
|
||||
}
|
||||
|
||||
func (ts *transitServer) Sign(ctx context.Context, req *pb.TransitSignRequest) (*pb.TransitSignResponse, error) {
|
||||
if req.Mount == "" || req.Key == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount and key are required")
|
||||
}
|
||||
resp, err := ts.transitHandleRequest(ctx, req.Mount, "sign", &engine.Request{
|
||||
Operation: "sign",
|
||||
CallerInfo: ts.callerInfo(ctx),
|
||||
CheckPolicy: ts.policyChecker(ctx),
|
||||
Data: map[string]interface{}{"key": req.Key, "input": req.Input},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sig, _ := resp.Data["signature"].(string)
|
||||
return &pb.TransitSignResponse{Signature: sig}, nil
|
||||
}
|
||||
|
||||
func (ts *transitServer) Verify(ctx context.Context, req *pb.TransitVerifyRequest) (*pb.TransitVerifyResponse, error) {
|
||||
if req.Mount == "" || req.Key == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount and key are required")
|
||||
}
|
||||
resp, err := ts.transitHandleRequest(ctx, req.Mount, "verify", &engine.Request{
|
||||
Operation: "verify",
|
||||
CallerInfo: ts.callerInfo(ctx),
|
||||
CheckPolicy: ts.policyChecker(ctx),
|
||||
Data: map[string]interface{}{
|
||||
"key": req.Key,
|
||||
"input": req.Input,
|
||||
"signature": req.Signature,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
valid, _ := resp.Data["valid"].(bool)
|
||||
return &pb.TransitVerifyResponse{Valid: valid}, nil
|
||||
}
|
||||
|
||||
func (ts *transitServer) Hmac(ctx context.Context, req *pb.TransitHmacRequest) (*pb.TransitHmacResponse, error) {
|
||||
if req.Mount == "" || req.Key == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount and key are required")
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"key": req.Key,
|
||||
"input": req.Input,
|
||||
}
|
||||
if req.Hmac != "" {
|
||||
data["hmac"] = req.Hmac
|
||||
}
|
||||
resp, err := ts.transitHandleRequest(ctx, req.Mount, "hmac", &engine.Request{
|
||||
Operation: "hmac",
|
||||
CallerInfo: ts.callerInfo(ctx),
|
||||
CheckPolicy: ts.policyChecker(ctx),
|
||||
Data: data,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hmacStr, _ := resp.Data["hmac"].(string)
|
||||
valid, _ := resp.Data["valid"].(bool)
|
||||
return &pb.TransitHmacResponse{Hmac: hmacStr, Valid: valid}, nil
|
||||
}
|
||||
|
||||
func (ts *transitServer) GetPublicKey(ctx context.Context, req *pb.GetTransitPublicKeyRequest) (*pb.GetTransitPublicKeyResponse, error) {
|
||||
if req.Mount == "" || req.Name == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount and name are required")
|
||||
}
|
||||
data := map[string]interface{}{"name": req.Name}
|
||||
if req.Version != 0 {
|
||||
data["version"] = float64(req.Version)
|
||||
}
|
||||
resp, err := ts.transitHandleRequest(ctx, req.Mount, "get-public-key", &engine.Request{
|
||||
Operation: "get-public-key",
|
||||
CallerInfo: ts.callerInfo(ctx),
|
||||
Data: data,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pk, _ := resp.Data["public_key"].(string)
|
||||
version, _ := resp.Data["version"].(int)
|
||||
keyType, _ := resp.Data["type"].(string)
|
||||
return &pb.GetTransitPublicKeyResponse{
|
||||
PublicKey: pk,
|
||||
Version: int32(version),
|
||||
Type: keyType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
func protoItemsToInterface(items []*pb.TransitBatchItem) []interface{} {
|
||||
out := make([]interface{}, len(items))
|
||||
for i, item := range items {
|
||||
m := map[string]interface{}{}
|
||||
if item.Plaintext != "" {
|
||||
m["plaintext"] = item.Plaintext
|
||||
}
|
||||
if item.Ciphertext != "" {
|
||||
m["ciphertext"] = item.Ciphertext
|
||||
}
|
||||
if item.Context != "" {
|
||||
m["context"] = item.Context
|
||||
}
|
||||
if item.Reference != "" {
|
||||
m["reference"] = item.Reference
|
||||
}
|
||||
out[i] = m
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func toBatchResponse(resp *engine.Response) *pb.TransitBatchResponse {
|
||||
raw, _ := resp.Data["results"].([]interface{})
|
||||
results := make([]*pb.TransitBatchResultItem, 0, len(raw))
|
||||
for _, item := range raw {
|
||||
switch r := item.(type) {
|
||||
case map[string]interface{}:
|
||||
pt, _ := r["plaintext"].(string)
|
||||
ct, _ := r["ciphertext"].(string)
|
||||
ref, _ := r["reference"].(string)
|
||||
errStr, _ := r["error"].(string)
|
||||
results = append(results, &pb.TransitBatchResultItem{
|
||||
Plaintext: pt,
|
||||
Ciphertext: ct,
|
||||
Reference: ref,
|
||||
Error: errStr,
|
||||
})
|
||||
}
|
||||
}
|
||||
return &pb.TransitBatchResponse{Results: results}
|
||||
}
|
||||
@@ -47,6 +47,26 @@ func (s *Server) registerRoutes(r chi.Router) {
|
||||
|
||||
r.HandleFunc("/v1/policy/rules", s.requireAuth(s.handlePolicyRules))
|
||||
r.HandleFunc("/v1/policy/rule", s.requireAuth(s.handlePolicyRule))
|
||||
|
||||
// Transit engine routes.
|
||||
r.Post("/v1/transit/{mount}/keys", s.requireAdmin(s.handleTransitCreateKey))
|
||||
r.Get("/v1/transit/{mount}/keys", s.requireAuth(s.handleTransitListKeys))
|
||||
r.Get("/v1/transit/{mount}/keys/{name}", s.requireAuth(s.handleTransitGetKey))
|
||||
r.Delete("/v1/transit/{mount}/keys/{name}", s.requireAdmin(s.handleTransitDeleteKey))
|
||||
r.Post("/v1/transit/{mount}/keys/{name}/rotate", s.requireAdmin(s.handleTransitRotateKey))
|
||||
r.Post("/v1/transit/{mount}/keys/{name}/config", s.requireAdmin(s.handleTransitUpdateKeyConfig))
|
||||
r.Post("/v1/transit/{mount}/keys/{name}/trim", s.requireAdmin(s.handleTransitTrimKey))
|
||||
r.Post("/v1/transit/{mount}/encrypt/{key}", s.requireAuth(s.handleTransitEncrypt))
|
||||
r.Post("/v1/transit/{mount}/decrypt/{key}", s.requireAuth(s.handleTransitDecrypt))
|
||||
r.Post("/v1/transit/{mount}/rewrap/{key}", s.requireAuth(s.handleTransitRewrap))
|
||||
r.Post("/v1/transit/{mount}/batch/encrypt/{key}", s.requireAuth(s.handleTransitBatchEncrypt))
|
||||
r.Post("/v1/transit/{mount}/batch/decrypt/{key}", s.requireAuth(s.handleTransitBatchDecrypt))
|
||||
r.Post("/v1/transit/{mount}/batch/rewrap/{key}", s.requireAuth(s.handleTransitBatchRewrap))
|
||||
r.Post("/v1/transit/{mount}/sign/{key}", s.requireAuth(s.handleTransitSign))
|
||||
r.Post("/v1/transit/{mount}/verify/{key}", s.requireAuth(s.handleTransitVerify))
|
||||
r.Post("/v1/transit/{mount}/hmac/{key}", s.requireAuth(s.handleTransitHmac))
|
||||
r.Get("/v1/transit/{mount}/keys/{name}/public-key", s.requireAuth(s.handleTransitGetPublicKey))
|
||||
|
||||
s.registerACMERoutes(r)
|
||||
}
|
||||
|
||||
@@ -608,11 +628,270 @@ func (s *Server) getCAEngine(mountName string) (*ca.CAEngine, error) {
|
||||
return caEng, nil
|
||||
}
|
||||
|
||||
// --- Transit Engine Handlers ---
|
||||
|
||||
func (s *Server) transitRequest(w http.ResponseWriter, r *http.Request, mount, operation string, data map[string]interface{}) {
|
||||
info := TokenInfoFromContext(r.Context())
|
||||
|
||||
policyChecker := func(resource, action string) (string, bool) {
|
||||
pReq := &policy.Request{
|
||||
Username: info.Username,
|
||||
Roles: info.Roles,
|
||||
Resource: resource,
|
||||
Action: action,
|
||||
}
|
||||
eff, matched, pErr := s.policy.Match(r.Context(), pReq)
|
||||
if pErr != nil {
|
||||
return string(policy.EffectDeny), false
|
||||
}
|
||||
return string(eff), matched
|
||||
}
|
||||
|
||||
resp, err := s.engines.HandleRequest(r.Context(), mount, &engine.Request{
|
||||
Operation: operation,
|
||||
CallerInfo: &engine.CallerInfo{Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin},
|
||||
CheckPolicy: policyChecker,
|
||||
Data: data,
|
||||
})
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
switch {
|
||||
case errors.Is(err, engine.ErrMountNotFound):
|
||||
status = http.StatusNotFound
|
||||
case strings.Contains(err.Error(), "forbidden"):
|
||||
status = http.StatusForbidden
|
||||
case strings.Contains(err.Error(), "authentication required"):
|
||||
status = http.StatusUnauthorized
|
||||
case strings.Contains(err.Error(), "not found"):
|
||||
status = http.StatusNotFound
|
||||
case strings.Contains(err.Error(), "not allowed"):
|
||||
status = http.StatusForbidden
|
||||
case strings.Contains(err.Error(), "unsupported"):
|
||||
status = http.StatusBadRequest
|
||||
case strings.Contains(err.Error(), "invalid"):
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
http.Error(w, `{"error":"`+err.Error()+`"}`, status)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp.Data)
|
||||
}
|
||||
|
||||
func (s *Server) handleTransitCreateKey(w http.ResponseWriter, r *http.Request) {
|
||||
mount := chi.URLParam(r, "mount")
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
if err := readJSON(r, &req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.transitRequest(w, r, mount, "create-key", map[string]interface{}{"name": req.Name, "type": req.Type})
|
||||
}
|
||||
|
||||
func (s *Server) handleTransitDeleteKey(w http.ResponseWriter, r *http.Request) {
|
||||
mount := chi.URLParam(r, "mount")
|
||||
name := chi.URLParam(r, "name")
|
||||
s.transitRequest(w, r, mount, "delete-key", map[string]interface{}{"name": name})
|
||||
}
|
||||
|
||||
func (s *Server) handleTransitGetKey(w http.ResponseWriter, r *http.Request) {
|
||||
mount := chi.URLParam(r, "mount")
|
||||
name := chi.URLParam(r, "name")
|
||||
s.transitRequest(w, r, mount, "get-key", map[string]interface{}{"name": name})
|
||||
}
|
||||
|
||||
func (s *Server) handleTransitListKeys(w http.ResponseWriter, r *http.Request) {
|
||||
mount := chi.URLParam(r, "mount")
|
||||
s.transitRequest(w, r, mount, "list-keys", nil)
|
||||
}
|
||||
|
||||
func (s *Server) handleTransitRotateKey(w http.ResponseWriter, r *http.Request) {
|
||||
mount := chi.URLParam(r, "mount")
|
||||
name := chi.URLParam(r, "name")
|
||||
s.transitRequest(w, r, mount, "rotate-key", map[string]interface{}{"name": name})
|
||||
}
|
||||
|
||||
func (s *Server) handleTransitUpdateKeyConfig(w http.ResponseWriter, r *http.Request) {
|
||||
mount := chi.URLParam(r, "mount")
|
||||
name := chi.URLParam(r, "name")
|
||||
var req struct {
|
||||
MinDecryptionVersion *float64 `json:"min_decryption_version"`
|
||||
AllowDeletion *bool `json:"allow_deletion"`
|
||||
}
|
||||
if err := readJSON(r, &req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
data := map[string]interface{}{"name": name}
|
||||
if req.MinDecryptionVersion != nil {
|
||||
data["min_decryption_version"] = *req.MinDecryptionVersion
|
||||
}
|
||||
if req.AllowDeletion != nil {
|
||||
data["allow_deletion"] = *req.AllowDeletion
|
||||
}
|
||||
s.transitRequest(w, r, mount, "update-key-config", data)
|
||||
}
|
||||
|
||||
func (s *Server) handleTransitTrimKey(w http.ResponseWriter, r *http.Request) {
|
||||
mount := chi.URLParam(r, "mount")
|
||||
name := chi.URLParam(r, "name")
|
||||
s.transitRequest(w, r, mount, "trim-key", map[string]interface{}{"name": name})
|
||||
}
|
||||
|
||||
func (s *Server) handleTransitEncrypt(w http.ResponseWriter, r *http.Request) {
|
||||
mount := chi.URLParam(r, "mount")
|
||||
key := chi.URLParam(r, "key")
|
||||
var req struct {
|
||||
Plaintext string `json:"plaintext"`
|
||||
Context string `json:"context"`
|
||||
}
|
||||
if err := readJSON(r, &req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
data := map[string]interface{}{"key": key, "plaintext": req.Plaintext}
|
||||
if req.Context != "" {
|
||||
data["context"] = req.Context
|
||||
}
|
||||
s.transitRequest(w, r, mount, "encrypt", data)
|
||||
}
|
||||
|
||||
func (s *Server) handleTransitDecrypt(w http.ResponseWriter, r *http.Request) {
|
||||
mount := chi.URLParam(r, "mount")
|
||||
key := chi.URLParam(r, "key")
|
||||
var req struct {
|
||||
Ciphertext string `json:"ciphertext"`
|
||||
Context string `json:"context"`
|
||||
}
|
||||
if err := readJSON(r, &req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
data := map[string]interface{}{"key": key, "ciphertext": req.Ciphertext}
|
||||
if req.Context != "" {
|
||||
data["context"] = req.Context
|
||||
}
|
||||
s.transitRequest(w, r, mount, "decrypt", data)
|
||||
}
|
||||
|
||||
func (s *Server) handleTransitRewrap(w http.ResponseWriter, r *http.Request) {
|
||||
mount := chi.URLParam(r, "mount")
|
||||
key := chi.URLParam(r, "key")
|
||||
var req struct {
|
||||
Ciphertext string `json:"ciphertext"`
|
||||
Context string `json:"context"`
|
||||
}
|
||||
if err := readJSON(r, &req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
data := map[string]interface{}{"key": key, "ciphertext": req.Ciphertext}
|
||||
if req.Context != "" {
|
||||
data["context"] = req.Context
|
||||
}
|
||||
s.transitRequest(w, r, mount, "rewrap", data)
|
||||
}
|
||||
|
||||
func (s *Server) handleTransitBatchEncrypt(w http.ResponseWriter, r *http.Request) {
|
||||
mount := chi.URLParam(r, "mount")
|
||||
key := chi.URLParam(r, "key")
|
||||
var req struct {
|
||||
Items []interface{} `json:"items"`
|
||||
}
|
||||
if err := readJSON(r, &req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.transitRequest(w, r, mount, "batch-encrypt", map[string]interface{}{"key": key, "items": req.Items})
|
||||
}
|
||||
|
||||
func (s *Server) handleTransitBatchDecrypt(w http.ResponseWriter, r *http.Request) {
|
||||
mount := chi.URLParam(r, "mount")
|
||||
key := chi.URLParam(r, "key")
|
||||
var req struct {
|
||||
Items []interface{} `json:"items"`
|
||||
}
|
||||
if err := readJSON(r, &req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.transitRequest(w, r, mount, "batch-decrypt", map[string]interface{}{"key": key, "items": req.Items})
|
||||
}
|
||||
|
||||
func (s *Server) handleTransitBatchRewrap(w http.ResponseWriter, r *http.Request) {
|
||||
mount := chi.URLParam(r, "mount")
|
||||
key := chi.URLParam(r, "key")
|
||||
var req struct {
|
||||
Items []interface{} `json:"items"`
|
||||
}
|
||||
if err := readJSON(r, &req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.transitRequest(w, r, mount, "batch-rewrap", map[string]interface{}{"key": key, "items": req.Items})
|
||||
}
|
||||
|
||||
func (s *Server) handleTransitSign(w http.ResponseWriter, r *http.Request) {
|
||||
mount := chi.URLParam(r, "mount")
|
||||
key := chi.URLParam(r, "key")
|
||||
var req struct {
|
||||
Input string `json:"input"`
|
||||
}
|
||||
if err := readJSON(r, &req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.transitRequest(w, r, mount, "sign", map[string]interface{}{"key": key, "input": req.Input})
|
||||
}
|
||||
|
||||
func (s *Server) handleTransitVerify(w http.ResponseWriter, r *http.Request) {
|
||||
mount := chi.URLParam(r, "mount")
|
||||
key := chi.URLParam(r, "key")
|
||||
var req struct {
|
||||
Input string `json:"input"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
if err := readJSON(r, &req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.transitRequest(w, r, mount, "verify", map[string]interface{}{"key": key, "input": req.Input, "signature": req.Signature})
|
||||
}
|
||||
|
||||
func (s *Server) handleTransitHmac(w http.ResponseWriter, r *http.Request) {
|
||||
mount := chi.URLParam(r, "mount")
|
||||
key := chi.URLParam(r, "key")
|
||||
var req struct {
|
||||
Input string `json:"input"`
|
||||
HMAC string `json:"hmac"`
|
||||
}
|
||||
if err := readJSON(r, &req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
data := map[string]interface{}{"key": key, "input": req.Input}
|
||||
if req.HMAC != "" {
|
||||
data["hmac"] = req.HMAC
|
||||
}
|
||||
s.transitRequest(w, r, mount, "hmac", data)
|
||||
}
|
||||
|
||||
func (s *Server) handleTransitGetPublicKey(w http.ResponseWriter, r *http.Request) {
|
||||
mount := chi.URLParam(r, "mount")
|
||||
name := chi.URLParam(r, "name")
|
||||
s.transitRequest(w, r, mount, "get-public-key", map[string]interface{}{"name": name})
|
||||
}
|
||||
|
||||
// operationAction maps an engine operation name to a policy action ("read" or "write").
|
||||
func operationAction(op string) string {
|
||||
switch op {
|
||||
case "list-issuers", "list-certs", "get-cert", "get-root", "get-chain", "get-issuer":
|
||||
case "list-issuers", "list-certs", "get-cert", "get-root", "get-chain", "get-issuer",
|
||||
"list-keys", "get-key", "get-public-key":
|
||||
return "read"
|
||||
case "decrypt", "rewrap", "batch-decrypt", "batch-rewrap":
|
||||
return "decrypt"
|
||||
default:
|
||||
return "write"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user