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 "bytes"
7 "crypto/elliptic"
8 "crypto/sha256"
9 "encoding/base64"
10 "fmt"
11 "io"
12 "os"
13 "sort"
14
15 family "amadan.net/rastrillo/rastrillo/crypto"
16 "github.com/go-json-experiment/json"
17 "github.com/go-json-experiment/json/jsontext"
18 "golang.org/x/sys/unix"
19)
20
21const floorContext = "oficina/provisioner/floor/v1"
22const maxRecords = 10000
23
24type floorIdentity struct {
25 Registry string `json:"registry"`
26 Device string `json:"device"`
27 Public string `json:"public_key"`
28}
29type floorPoint struct {
30 Sequence int64 `json:"sequence"`
31 Head string `json:"head"`
32}
33type floorRecord struct {
34 Version int `json:"version"`
35 Identity floorIdentity `json:"identity"`
36 Point floorPoint `json:"point"`
37 Previous string `json:"previous"`
38}
39type floorEnvelope struct {
40 Record floorRecord `json:"record"`
41 Signature string `json:"signature"`
42}
43type nativeFloor struct {
44 dir *privateDirectory
45 identity floorIdentity
46 public []byte
47 markerBoundary func() error
48}
49
50func tokenBytes(s string, n int) ([]byte, bool) {
51 b, e := base64.RawURLEncoding.DecodeString(s)
52 return b, e == nil && len(b) == n && base64.RawURLEncoding.EncodeToString(b) == s
53}
54func canonicalJSON(v any) ([]byte, error) {
55 b, e := json.Marshal(v)
56 if e != nil {
57 return nil, ErrRefused
58 }
59 j := jsontext.Value(b)
60 if j.Canonicalize(jsontext.CanonicalizeRawInts(false)) != nil {
61 return nil, ErrRefused
62 }
63 return j, nil
64}
65func closedJSON(b []byte, v any) error {
66 if json.Unmarshal(b, v, json.RejectUnknownMembers(true)) != nil {
67 return ErrRefused
68 }
69 canonical, e := canonicalJSON(v)
70 if e != nil || !bytes.Equal(canonical, b) {
71 return ErrRefused
72 }
73 return nil
74}
75func hash(b []byte) string { h := sha256.Sum256(b); return base64.RawURLEncoding.EncodeToString(h[:]) }
76func floorName(sequence int64) string { return fmt.Sprintf("%020d.json", sequence) }
77func newNativeFloor(dir *privateDirectory, id floorIdentity) (*nativeFloor, error) {
78 _, a := tokenBytes(id.Registry, 32)
79 _, b := tokenBytes(id.Device, 32)
80 pub, c := tokenBytes(id.Public, 65)
81 x, y := elliptic.Unmarshal(elliptic.P256(), pub)
82 if dir == nil || !a || !b || !c || x == nil || y == nil {
83 return nil, ErrRefused
84 }
85 return &nativeFloor{dir: dir, identity: id, public: pub}, nil
86}
87func (f *nativeFloor) signed(k *family.Keypair, p floorPoint, previous string) ([]byte, error) {
88 if k == nil || k.SignPriv == nil || !bytes.Equal(k.SignPub(), f.public) {
89 return nil, ErrRefused
90 }
91 r := floorRecord{1, f.identity, p, previous}
92 body, e := canonicalJSON(r)
93 if e != nil {
94 return nil, e
95 }
96 sig, e := family.Sign(k, floorContext, body)
97 if e != nil {
98 return nil, ErrUnavailable
99 }
100 return canonicalJSON(floorEnvelope{r, base64.RawURLEncoding.EncodeToString(sig)})
101}
102func (f *nativeFloor) initialize(k *family.Keypair) error {
103 files, e := directoryNames(f.dir)
104 if e != nil {
105 return e
106 }
107 for _, name := range files {
108 if name != "lock" {
109 return ErrRefused
110 }
111 }
112 raw, e := f.signed(k, floorPoint{}, "")
113 if e != nil {
114 return e
115 }
116 if e = f.dir.create(floorName(0), raw); e != nil {
117 return e
118 }
119 return f.updateMarker(floorPoint{})
120}
121func (f *nativeFloor) names() ([]string, error) {
122 entries, e := directoryNames(f.dir)
123 if e != nil {
124 return nil, e
125 }
126 names := make([]string, 0, len(entries))
127 for _, name := range entries {
128 if name == "lock" || name == "head" || name == "head.tmp" {
129 continue
130 }
131 if len(name) != 25 || name[20:] != ".json" {
132 return nil, ErrRefused
133 }
134 names = append(names, name)
135 }
136 sort.Strings(names)
137 return names, nil
138}
139func (f *nativeFloor) current() (floorPoint, string, error) {
140 names, e := f.names()
141 if e != nil {
142 return floorPoint{}, "", e
143 }
144 if len(names) == 0 || len(names) > maxRecords+1 {
145 return floorPoint{}, "", ErrRefused
146 }
147 var last floorPoint
148 var previous string
149 for i, name := range names {
150 if name != floorName(int64(i)) {
151 return floorPoint{}, "", ErrRefused
152 }
153 raw, e := f.dir.read(name, 8192)
154 if e != nil {
155 return floorPoint{}, "", e
156 }
157 var env floorEnvelope
158 if closedJSON(raw, &env) != nil || env.Record.Version != 1 || env.Record.Identity != f.identity || env.Record.Point.Sequence != int64(i) || env.Record.Previous != previous {
159 return floorPoint{}, "", ErrRefused
160 }
161 r := env.Record
162 if i == 0 {
163 if r.Point.Head != "" {
164 return floorPoint{}, "", ErrRefused
165 }
166 } else {
167 if _, ok := tokenBytes(r.Point.Head, 32); !ok {
168 return floorPoint{}, "", ErrRefused
169 }
170 }
171 sig, ok := tokenBytes(env.Signature, 64)
172 body, e := canonicalJSON(r)
173 if !ok || e != nil || !family.Verify(f.public, floorContext, body, sig) {
174 return floorPoint{}, "", ErrRefused
175 }
176 previous = hash(raw)
177 last = r.Point
178 }
179 return last, previous, nil
180}
181
182// advance requires the caller to hold the device process lock. It checks every
183// immutable floor record again, then exclusively creates the adjacent record.
184func (f *nativeFloor) advance(k *family.Keypair, expected, next floorPoint) error {
185 current, previous, e := f.current()
186 if e != nil {
187 return e
188 }
189 if current != expected || next.Sequence != expected.Sequence+1 || next.Sequence > maxRecords {
190 return ErrRefused
191 }
192 if _, ok := tokenBytes(next.Head, 32); !ok {
193 return ErrRefused
194 }
195 raw, e := f.signed(k, next, previous)
196 if e != nil {
197 return e
198 }
199 if e = f.dir.create(floorName(next.Sequence), raw); e != nil {
200 return e
201 }
202 return f.updateMarker(next)
203}
204
205// The marker is an optimization only. Every open scans signed immutable records,
206// including a durable newer record written before a marker-update crash.
207func (f *nativeFloor) updateMarker(point floorPoint) error {
208 raw, e := canonicalJSON(point)
209 if e != nil {
210 return e
211 }
212 if e = f.dir.create("head.tmp", raw); e != nil {
213 existing, readErr := f.dir.read("head.tmp", 8192)
214 if readErr != nil {
215 return ErrRefused
216 }
217 if !bytes.Equal(existing, raw) {
218 // Only the signed immutable chain carries authority. This private,
219 // regular scratch file can survive a marker rename crash; unlinking
220 // its directory entry never opens or truncates a replacement target.
221 if unix.Unlinkat(int(f.dir.file.Fd()), "head.tmp", 0) != nil || f.dir.file.Sync() != nil {
222 return ErrUnavailable
223 }
224 if e = f.dir.create("head.tmp", raw); e != nil {
225 return e
226 }
227 }
228 }
229 if f.markerBoundary != nil {
230 if e := f.markerBoundary(); e != nil {
231 return e
232 }
233 }
234 if unix.Renameat(int(f.dir.file.Fd()), "head.tmp", int(f.dir.file.Fd()), "head") != nil || f.dir.file.Sync() != nil {
235 return ErrUnavailable
236 }
237 return nil
238}
239
240func directoryNames(dir *privateDirectory) ([]string, error) {
241 // Open a fresh directory description: a retained Readdir offset must never
242 // hide files appended since a previous verification.
243 fd, e := openPrivateDirectory(dir.file.Name())
244 if e != nil {
245 return nil, e
246 }
247 defer fd.close()
248 oldInfo, e := dir.file.Stat()
249 if e != nil {
250 return nil, ErrUnavailable
251 }
252 newInfo, e := fd.file.Stat()
253 if e != nil || !os.SameFile(oldInfo, newInfo) {
254 return nil, ErrRefused
255 }
256 entries, e := fd.file.Readdirnames(maxRecords + 5)
257 if e != nil && e != io.EOF {
258 return nil, ErrUnavailable
259 }
260 if len(entries) > maxRecords+4 {
261 return nil, ErrRefused
262 }
263 return entries, nil
264}
265