// package bootprotocol contains the elements necessary to use the custom network boot protocol package bootprotocol import ( "bytes" "encoding" "errors" "fmt" "github.com/google/uuid" ) type Action int8 const ( ActionRequest Action = iota ActionAccept ActionDeny ActionUnknown ) var spaceByte = []byte(" ") var commaByte = []byte(",") const ( keyID = "id" keyEfiApp = "efi_app" keyReason = "reason" ) var ErrInvalidFormat = errors.New("invalid format for message") var ErrUnknownAction = errors.New("unknown action for message") var ErrInvalidParam = errors.New("invalid parameter for message") var ErrMissingParam = errors.New("missing parameter for message") func (a Action) String() string { switch a { case ActionAccept: return "BOOT_ACCEPT" case ActionDeny: return "BOOT_DENY" case ActionRequest: return "BOOT_REQUEST" default: return "unknown" } } func newActionFromBytes(raw []byte) Action { switch string(raw) { case "BOOT_ACCEPT": return ActionAccept case "BOOT_DENY": return ActionDeny case "BOOT_REQUEST": return ActionRequest default: return ActionUnknown } } type Message interface { encoding.BinaryUnmarshaler encoding.BinaryMarshaler Action() Action ID() uuid.UUID } type requestMessage struct { id uuid.UUID } func (rm *requestMessage) UnmarshalBinary(data []byte) error { params := bytes.Split(data, commaByte) for _, p := range params { k, v, err := splitKeyValue(p) if err != nil { return fmt.Errorf("failed to parse parameter %q: %w", string(p), err) } if bytes.Equal(k, []byte(keyID)) { parsedId, err := uuid.ParseBytes(v) if err != nil { return ErrInvalidParam } rm.id = parsedId return nil } } return ErrMissingParam } func (rm *requestMessage) MarshalBinary() (data []byte, err error) { action := []byte(rm.Action().String()) params := []byte(fmt.Sprintf("%s=%s", keyID, rm.id.String())) return bytes.Join([][]byte{action, params}, spaceByte), nil } func (rm *requestMessage) Action() Action { return ActionRequest } func (rm *requestMessage) ID() uuid.UUID { return rm.id } type acceptMessage struct { id uuid.UUID efiApp string } func (am *acceptMessage) UnmarshalBinary(data []byte) error { params := bytes.Split(data, commaByte) for _, p := range params { k, v, err := splitKeyValue(p) if err != nil { return fmt.Errorf("failed to parse parameter %q: %w", string(p), err) } switch string(k) { case keyID: parsedId, err := uuid.ParseBytes(v) if err != nil { return ErrInvalidParam } am.id = parsedId case keyEfiApp: am.efiApp = string(v) } } if am.id == uuid.Nil || am.efiApp == "" { return ErrMissingParam } return nil } func (am *acceptMessage) MarshalBinary() (data []byte, err error) { action := []byte(am.Action().String()) params := [][]byte{ []byte(fmt.Sprintf("%s=%s", keyID, am.id.String())), []byte(fmt.Sprintf("%s=%s", keyEfiApp, am.efiApp)), } param_bytes := bytes.Join(params, commaByte) return bytes.Join([][]byte{action, param_bytes}, spaceByte), nil } func (am *acceptMessage) Action() Action { return ActionAccept } func (am *acceptMessage) ID() uuid.UUID { return am.id } type denyMessage struct { id uuid.UUID reason string } func (dm *denyMessage) UnmarshalBinary(data []byte) error { params := bytes.Split(data, commaByte) for _, p := range params { k, v, err := splitKeyValue(p) if err != nil { return fmt.Errorf("failed to parse parameter %q: %w", string(p), err) } switch string(k) { case keyID: parsedId, err := uuid.ParseBytes(v) if err != nil { return ErrInvalidParam } dm.id = parsedId case keyReason: dm.reason = string(v) } } if dm.id == uuid.Nil || dm.reason == "" { return ErrMissingParam } return nil } func (dm *denyMessage) MarshalBinary() (data []byte, err error) { action := []byte(dm.Action().String()) params := [][]byte{ []byte(fmt.Sprintf("%s=%s", keyID, dm.id.String())), []byte(fmt.Sprintf("%s=%s", keyReason, dm.reason)), } param_bytes := bytes.Join(params, commaByte) return bytes.Join([][]byte{action, param_bytes}, spaceByte), nil } func (dm *denyMessage) Action() Action { return ActionDeny } func (dm *denyMessage) ID() uuid.UUID { return dm.id } func MessageFromBytes(dat []byte) (Message, error) { rawAction, content, found := bytes.Cut(dat, spaceByte) if !found { return nil, ErrInvalidFormat } var message Message action := newActionFromBytes(rawAction) switch action { case ActionRequest: message = &requestMessage{} case ActionAccept: message = &acceptMessage{} case ActionDeny: message = &denyMessage{} default: return nil, ErrUnknownAction } if err := message.UnmarshalBinary(content); err != nil { return nil, fmt.Errorf("failed to parse message: %w", err) } return message, nil } func Accept(id uuid.UUID, efiApp string) Message { return &acceptMessage{ id: id, efiApp: efiApp, } } func Deny(id uuid.UUID, reason string) Message { return &denyMessage{ id: id, reason: reason, } }