Skip to content
28 changes: 18 additions & 10 deletions byoc/job_orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -566,18 +566,26 @@ func (bso *BYOCOrchestratorServer) verifyJobCreds(ctx context.Context, jobCreds
return nil, errSegSig
}

if !bso.orch.VerifySig(ethcommon.HexToAddress(jobData.Sender), jobData.Request+jobData.Parameters, sigByte) {
clog.Errorf(ctx, "Sig check failed sender=%v", jobData.Sender)
return nil, errSegSig
}

if reserveCapacity && bso.orch.ReserveExternalCapabilityCapacity(jobData.Capability) != nil {
return nil, errZeroCapacity
sender := ethcommon.HexToAddress(jobData.Sender)

// Verify V1 structured binary format (matches DMZ /sign-byoc-job signing).
v1Payload := FlattenBYOCJob(&BYOCJobSigningInput{
ID: jobData.ID,
Capability: jobData.Capability,
Request: jobData.Request,
Parameters: jobData.Parameters,
TimeoutSeconds: jobData.Timeout,
})
if bso.orch.VerifySig(sender, string(v1Payload), sigByte) {
if reserveCapacity && bso.orch.ReserveExternalCapabilityCapacity(jobData.Capability) != nil {
return nil, errZeroCapacity
}
jobData.CapabilityUrl = bso.orch.GetUrlForCapability(jobData.Capability)
return jobData, nil
}

jobData.CapabilityUrl = bso.orch.GetUrlForCapability(jobData.Capability)

return jobData, nil
clog.Errorf(ctx, "Sig check failed sender=%v", jobData.Sender)
return nil, errSegSig
}

func (bso *BYOCOrchestratorServer) verifyTokenCreds(ctx context.Context, tokenCreds string) (*JobSender, error) {
Expand Down
63 changes: 63 additions & 0 deletions byoc/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package byoc
import (
"context"
"crypto/tls"
"encoding/binary"
"errors"
"math/big"
gonet "net"
Expand Down Expand Up @@ -279,3 +280,65 @@ type byocLiveRequestParams struct {
// when the write for the last segment started
lastSegmentTime time.Time
}

// Prevents cross-protocol signature replay.
const BYOCJobSigV1Prefix = "LP_BYOC_JOB_V1\x00\x00"

// BYOCJobSigningInput holds the fields that are bound into a BYOC job signature.
type BYOCJobSigningInput struct {
ID string
Capability string
Request string
Parameters string
TimeoutSeconds int
}

// FlattenBYOCJob produces a deterministic binary representation of a BYOC job
// for signing, similar to SegTranscodingMetadata.Flatten() used by LV2V.
//
// Wire format:
//
// version(16) || timeout(4,BE) || len(id)(4,BE) || id || len(cap)(4,BE) || cap
// || len(req)(4,BE) || req || len(params)(4,BE) || params
func FlattenBYOCJob(job *BYOCJobSigningInput) []byte {
idBytes := []byte(job.ID)
capBytes := []byte(job.Capability)
reqBytes := []byte(job.Request)
paramsBytes := []byte(job.Parameters)

size := 16 + 4 +
4 + len(idBytes) +
4 + len(capBytes) +
4 + len(reqBytes) +
4 + len(paramsBytes)

buf := make([]byte, size)
offset := 0

copy(buf[offset:], []byte(BYOCJobSigV1Prefix))
offset += 16

binary.BigEndian.PutUint32(buf[offset:], uint32(job.TimeoutSeconds))
offset += 4

binary.BigEndian.PutUint32(buf[offset:], uint32(len(idBytes)))
offset += 4
copy(buf[offset:], idBytes)
offset += len(idBytes)

binary.BigEndian.PutUint32(buf[offset:], uint32(len(capBytes)))
offset += 4
copy(buf[offset:], capBytes)
offset += len(capBytes)

binary.BigEndian.PutUint32(buf[offset:], uint32(len(reqBytes)))
offset += 4
copy(buf[offset:], reqBytes)
offset += len(reqBytes)

binary.BigEndian.PutUint32(buf[offset:], uint32(len(paramsBytes)))
offset += 4
copy(buf[offset:], paramsBytes)

return buf
}
1 change: 1 addition & 0 deletions cmd/livepeer/starter/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ func NewLivepeerConfig(fs *flag.FlagSet) LivepeerConfig {
cfg.RemoteSignerWebhookHeaders = fs.String("remoteSignerWebhookHeaders", *cfg.RemoteSignerWebhookHeaders, "Map of headers to use for remote signer webhook requests. e.g. 'header:val,header2:val2'")
cfg.RemoteSignerAllowNoAuth = fs.Bool("remoteSignerAllowNoAuth", *cfg.RemoteSignerAllowNoAuth, "Allow an unauthenticated remote signer on a public -httpAddr (no webhook). UNSAFE: signs payments from this node's deposit for any reachable caller; restrict access externally (proxy/private network).")
cfg.RemoteDiscovery = fs.Bool("remoteDiscovery", *cfg.RemoteDiscovery, "Enable orchestrator discovery on remote signers")
cfg.ByocPerCapPricing = fs.Bool("byocPerCapPricing", *cfg.ByocPerCapPricing, "Remote signer: resolve BYOC live-payment fee from the orchestrator's per-capability CapabilitiesPrices (keyed on the request capability) instead of the flat base price. Default OFF (byte-identical to base-price behavior).")

// Gateway metrics
cfg.KafkaBootstrapServers = fs.String("kafkaBootstrapServers", *cfg.KafkaBootstrapServers, "URL of Kafka Bootstrap Servers")
Expand Down
6 changes: 6 additions & 0 deletions cmd/livepeer/starter/starter.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ type LivepeerConfig struct {
RemoteSignerWebhookHeaders *string
RemoteSignerAllowNoAuth *bool
RemoteDiscovery *bool
ByocPerCapPricing *bool
AIRunnerImage *string
AIRunnerImageOverrides *string
AIVerboseLogs *bool
Expand Down Expand Up @@ -328,6 +329,7 @@ func DefaultLivepeerConfig() LivepeerConfig {
defaultRemoteSignerWebhookHeaders := ""
defaultRemoteSignerAllowNoAuth := false
defaultRemoteDiscovery := false
defaultByocPerCapPricing := false

// Gateway logs
defaultKafkaBootstrapServers := ""
Expand Down Expand Up @@ -461,6 +463,7 @@ func DefaultLivepeerConfig() LivepeerConfig {
RemoteSignerWebhookHeaders: &defaultRemoteSignerWebhookHeaders,
RemoteSignerAllowNoAuth: &defaultRemoteSignerAllowNoAuth,
RemoteDiscovery: &defaultRemoteDiscovery,
ByocPerCapPricing: &defaultByocPerCapPricing,

// Gateway logs
KafkaBootstrapServers: &defaultKafkaBootstrapServers,
Expand Down Expand Up @@ -1886,6 +1889,9 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) {
if cfg.RemoteDiscovery != nil {
n.RemoteDiscovery = *cfg.RemoteDiscovery
}
if cfg.ByocPerCapPricing != nil {
n.ByocPerCapPricing = *cfg.ByocPerCapPricing
}
if cfg.LiveAIHeartbeatHeaders != nil {
n.LiveAIHeartbeatHeaders = parseHeaderMap(*cfg.LiveAIHeartbeatHeaders)
}
Expand Down
1 change: 1 addition & 0 deletions core/livepeernode.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ type LivepeerNode struct {
RemoteEthAddr ethcommon.Address // eth address of the remote signer
InfoSig []byte // sig over eth address for the OrchestratorInfo request
RemoteDiscovery bool // expose remote discovery endpoint when enabled
ByocPerCapPricing bool // resolve BYOC fee from per-capability CapabilitiesPrices instead of base price (remote signer; default OFF)

// Thread safety for config fields
mu sync.RWMutex
Expand Down
Loading
Loading