oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package provisioner
4
5import (
6 "context"
7 "io"
8 "net/mail"
9 "os"
10 "strconv"
11 "time"
12
13 "amadan.net/oficina/home/internal/operator"
14 "github.com/go-json-experiment/json"
15 "github.com/go-json-experiment/json/jsontext"
16 "golang.org/x/sys/unix"
17)
18
19type OperationPlan struct {
20 Version int `json:"version"`
21 Installation string `json:"installation"`
22 Role string `json:"role"`
23 Operation string `json:"operation_id"`
24 Action string `json:"action"`
25 Input jsontext.Value `json:"input"`
26}
27type PreparationInfo struct {
28 Installation string `json:"installation"`
29 Operation string `json:"operation_id"`
30 Role string `json:"role"`
31 Sequence int64 `json:"sequence"`
32 State string `json:"state"`
33}
34
35// privateRequestOutput accepts an explicitly inherited anonymous write pipe.
36// Regular files, sockets, terminals and aliases of standard/input descriptors
37// are refused before any credential is unlocked or request is prepared.
38type requestOutput struct{ file *os.File }
39
40func (o *requestOutput) Close() error { return o.file.Close() }
41func privateRequestOutput(fd int, excluded ...int) (*requestOutput, error) {
42 if fd < 3 {
43 return nil, ErrRefused
44 }
45 var s unix.Stat_t
46 if unix.Fstat(fd, &s) != nil || s.Mode&unix.S_IFMT != unix.S_IFIFO {
47 return nil, ErrRefused
48 }
49 flags, e := unix.FcntlInt(uintptr(fd), unix.F_GETFL, 0)
50 if e != nil || flags&unix.O_ACCMODE != unix.O_WRONLY {
51 return nil, ErrRefused
52 }
53 name, e := os.Readlink("/proc/self/fd/" + strconv.Itoa(fd))
54 if e != nil || name != "pipe:["+strconv.FormatUint(s.Ino, 10)+"]" {
55 return nil, ErrRefused
56 }
57 for _, other := range append([]int{0, 1, 2}, excluded...) {
58 if other < 0 {
59 continue
60 }
61 var x unix.Stat_t
62 if unix.Fstat(other, &x) != nil || (x.Dev == s.Dev && x.Ino == s.Ino) {
63 return nil, ErrRefused
64 }
65 }
66 // Pollable nonblocking ownership lets Close interrupt a blocked Write.
67 // This descriptor is explicitly transferred to the native command.
68 if unix.SetNonblock(fd, true) != nil {
69 return nil, ErrRefused
70 }
71 file := os.NewFile(uintptr(fd), "private-request")
72 if file == nil {
73 return nil, ErrRefused
74 }
75 return &requestOutput{file}, nil
76}
77func decodeOperationPlan(raw []byte) (OperationPlan, any, error) {
78 var p OperationPlan
79 if len(raw) == 0 || len(raw) > operator.BodyLimit || json.Unmarshal(raw, &p, json.RejectUnknownMembers(true)) != nil || p.Version != 1 || !operator.CanonicalToken(p.Installation) || !operator.CanonicalToken(p.Operation) || len(p.Input) == 0 || len(p.Input) > operator.InputLimit {
80 return p, nil, ErrRefused
81 }
82 var input any
83 var base *operator.InputBase
84 switch p.Action {
85 case "directory.initialize":
86 in := new(operator.InitializeInput)
87 input = in
88 base = &in.InputBase
89 case "credential.owner-seed":
90 in := new(operator.OwnerInput)
91 input = in
92 base = &in.InputBase
93 case "directory.seed-team":
94 in := new(operator.SeedInput)
95 input = in
96 base = &in.InputBase
97 case "directory.provision-consumer":
98 in := new(operator.ProvisionInput)
99 input = in
100 base = &in.InputBase
101 default:
102 // Verification and activation await checked record/evidence collectors.
103 return p, nil, ErrRefused
104 }
105 role := "directory"
106 if p.Action == "credential.owner-seed" {
107 role = "credential"
108 }
109 if p.Role != role || json.Unmarshal(p.Input, input, json.RejectUnknownMembers(true)) != nil || base.Version != 1 || base.Operation != p.Action || base.OperationID != p.Operation {
110 return p, nil, ErrRefused
111 }
112 return p, input, nil
113}
114func preparationScope(s *installationState, input any) bool {
115 if s == nil {
116 return false
117 }
118 switch in := input.(type) {
119 case *operator.InitializeInput:
120 return in.ExpectedGeneration == nil && operator.CanonicalToken(in.Generation) && in.Baseline == "operator-attests-never-provisioned"
121 case *operator.OwnerInput:
122 sqid, e := operator.TeamSQID(in.TeamID)
123 address, parseErr := mail.ParseAddress(in.Address)
124 return e == nil && parseErr == nil && address.Address == in.Address && address.Name == "" && in.ExistingOnly && in.TeamSQID == sqid && in.Directory == s.Plan.Directory.Origin && len(in.Address) > 0 && len(in.Address) <= 320
125 case *operator.SeedInput:
126 return in.Generation == s.Generation && s.Generation != ""
127 case *operator.ProvisionInput:
128 return in.Generation == s.Generation && s.Generation != ""
129 }
130 return false
131}
132func prepareRequest(ctx context.Context, statePath, floorPath string, password, plan []byte, output *requestOutput) (PreparationInfo, error) {
133 p, input, e := decodeOperationPlan(plan)
134 if e != nil || output == nil {
135 return PreparationInfo{}, ErrRefused
136 }
137 j, e := existingJournal(statePath, floorPath, password)
138 if e != nil {
139 return PreparationInfo{}, e
140 }
141 defer closeExisting(j)
142 j.mu.Lock()
143 b, e := j.bookLocked()
144 j.mu.Unlock()
145 if e != nil {
146 return PreparationInfo{}, e
147 }
148 s := b.installations[p.Installation]
149 if !preparationScope(s, input) {
150 return PreparationInfo{}, ErrRefused
151 }
152 raw, e := j.prepare(ctx, p.Installation, p.Operation, p.Action, input)
153 if e != nil {
154 return PreparationInfo{}, e
155 }
156 defer clear(raw)
157 req, e := j.request(s, raw, p.Action)
158 if e != nil {
159 return PreparationInfo{}, e
160 }
161 // The record and independent floor are durable before output. Failure cannot
162 // release its role slot or cause the next call to sign a replacement.
163 if e = output.write(ctx, raw); e != nil {
164 return PreparationInfo{}, e
165 }
166 return PreparationInfo{p.Installation, p.Operation, p.Role, req.Authorization().Sequence, "local-request"}, nil
167}
168func writePrivateRequest(output io.Writer, raw []byte) error {
169 if output == nil || len(raw) == 0 || len(raw) > operator.BodyLimit {
170 return ErrRefused
171 }
172 n, e := output.Write(raw)
173 if e != nil || n != len(raw) {
174 return ErrUnavailable
175 }
176 return nil
177}
178
179// write keeps a stalled recipient from retaining the journal's process lock.
180// Closing a pollable pipe interrupts its wait without discarding the saved
181// request. The recipient may have a prefix, so a retry still uses exact bytes.
182func (o *requestOutput) write(ctx context.Context, raw []byte) error {
183 if o == nil || o.file == nil || len(raw) == 0 || len(raw) > operator.BodyLimit {
184 return ErrRefused
185 }
186 bounded, cancel := context.WithTimeout(ctx, 30*time.Second)
187 defer cancel()
188 stop := context.AfterFunc(bounded, func() { o.Close() })
189 defer stop()
190 e := writePrivateRequest(o.file, raw)
191 if bounded.Err() != nil {
192 return ErrUnavailable
193 }
194 return e
195}
196