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 home
4
5import (
6 "bytes"
7 "context"
8 "crypto/elliptic"
9 "crypto/sha256"
10 "encoding/base64"
11 "encoding/hex"
12 "os"
13 "path/filepath"
14 "time"
15
16 "amadan.net/oficina/home/internal/home/directorydata"
17 "amadan.net/oficina/home/internal/home/seal"
18 "amadan.net/oficina/home/internal/operator"
19 "amadan.net/oficina/home/roster"
20 "amadan.net/rastrillo/rastrillo/assertion"
21 json "github.com/go-json-experiment/json"
22)
23
24type credentialOperatorMaterial struct {
25 Version int `json:"version"`
26 Role string `json:"role"`
27 MAC string `json:"mac_key"`
28 Owner string `json:"owner_seed_key"`
29}
30type directoryOperatorMaterial struct {
31 Version int `json:"version"`
32 Role string `json:"role"`
33 MAC string `json:"mac_key"`
34 Owner string `json:"owner_seed_public"`
35 Provisioning string `json:"provisioning_key"`
36 Roster []roster.Key `json:"roster_keys"`
37}
38
39// RunOperatorCommand has no serving startup, migration, mailer or logger.
40func RunOperatorCommand(ctx context.Context, path string) (code int) {
41 code = 3
42 defer func() {
43 if recover() != nil {
44 code = 3
45 }
46 }()
47 if ctx == nil || !filepath.IsAbs(path) || filepath.Clean(path) != path || path == "/" {
48 return 2
49 }
50 material, err := loadOperatorMaterial()
51 defer clear(material.MAC)
52 if err != nil {
53 return 2
54 }
55 ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
56 defer cancel()
57 result, err := operator.RunCommand(ctx, path, material)
58 if err == operator.ErrRefused {
59 return 2
60 }
61 if err != nil {
62 return 3
63 }
64 if result.Write(ctx) != nil {
65 return 3
66 }
67 return 0
68}
69
70func operatorMAC(raw string) ([]byte, error) {
71 key, err := base64.StdEncoding.Strict().DecodeString(raw)
72 if err != nil || len(key) != 32 || base64.StdEncoding.EncodeToString(key) != raw || bytes.Equal(key, make([]byte, 32)) {
73 return nil, operator.ErrRefused
74 }
75 return key, nil
76}
77func operatorPublic(raw string) ([]byte, error) {
78 key, err := base64.RawURLEncoding.Strict().DecodeString(raw)
79 if err != nil || len(key) != 65 || base64.RawURLEncoding.EncodeToString(key) != raw {
80 return nil, operator.ErrRefused
81 }
82 x, _ := elliptic.Unmarshal(elliptic.P256(), key)
83 if x == nil {
84 return nil, operator.ErrRefused
85 }
86 return key, nil
87}
88func distinctOperatorKeys(keys [][]byte) bool {
89 seen := map[string]bool{}
90 for _, key := range keys {
91 if len(key) == 0 || seen[string(key)] {
92 return false
93 }
94 seen[string(key)] = true
95 }
96 return true
97}
98
99func loadOperatorMaterial() (operator.CommandMaterial, error) {
100 var material operator.CommandMaterial
101 role := os.Getenv("HOME_ROLE")
102 if role != "credential" && role != "directory" {
103 return material, operator.ErrRefused
104 }
105 origins, err := LoadOrigins()
106 if err != nil {
107 return material, operator.ErrRefused
108 }
109 raw, err := exchangeJSON("HOME_OPERATOR_MATERIAL", 16384)
110 if err != nil || raw == "" {
111 return material, operator.ErrRefused
112 }
113 dek, err := key32("HOME_DEK")
114 if err != nil {
115 return material, operator.ErrRefused
116 }
117 defer clear(dek[:])
118 sealer, err := seal.New(dek[:])
119 if err != nil {
120 return material, operator.ErrRefused
121 }
122 check, err := operatorSchemaCheck(role)
123 if err != nil {
124 return material, err
125 }
126 material = operator.CommandMaterial{Role: role, Sealer: sealer, CheckSchema: check}
127 if role == "credential" {
128 var cfg credentialOperatorMaterial
129 if json.Unmarshal([]byte(raw), &cfg, json.RejectUnknownMembers(true)) != nil || cfg.Version != 1 || cfg.Role != role {
130 return material, operator.ErrRefused
131 }
132 material.MAC, err = operatorMAC(cfg.MAC)
133 if err != nil || bytes.Equal(material.MAC, dek[:]) {
134 return material, operator.ErrRefused
135 }
136 pepper, err := key32("HOME_PEPPER")
137 if err != nil || bytes.Equal(pepper[:], dek[:]) || bytes.Equal(pepper[:], material.MAC) {
138 return material, operator.ErrRefused
139 }
140 defer clear(pepper[:])
141 resolve, err := NewOperatorPersonResolver(pepper[:])
142 if err != nil {
143 return material, operator.ErrRefused
144 }
145 owner, err := decodeHomeKeypair(cfg.Owner)
146 if err != nil {
147 return material, operator.ErrRefused
148 }
149 browser, err := decodeHomeKeypair(os.Getenv("HOME_ASSERTION_KEY"))
150 if err != nil {
151 return material, operator.ErrRefused
152 }
153 material.OtherKeys = [][]byte{owner.SignPub(), browser.SignPub()}
154 material.Origin = origins.Credential
155 material.Build = func(engine *operator.Engine) (operator.CommandExecutor, error) {
156 return operator.NewCredential(engine, origins.Directory, owner, [][]byte{browser.SignPub()}, resolve)
157 }
158 } else {
159 var cfg directoryOperatorMaterial
160 if json.Unmarshal([]byte(raw), &cfg, json.RejectUnknownMembers(true)) != nil || cfg.Version != 1 || cfg.Role != role || len(cfg.Roster) == 0 || len(cfg.Roster) > 8 {
161 return material, operator.ErrRefused
162 }
163 material.MAC, err = operatorMAC(cfg.MAC)
164 if err != nil || bytes.Equal(material.MAC, dek[:]) {
165 return material, operator.ErrRefused
166 }
167 owner, err := operator.NewOwnerVerifier(cfg.Owner, origins.Credential, origins.Directory)
168 if err != nil {
169 return material, operator.ErrRefused
170 }
171 ownerPub, err := operatorPublic(cfg.Owner)
172 if err != nil {
173 return material, operator.ErrRefused
174 }
175 signer, err := decodeHomeKeypair(cfg.Provisioning)
176 if err != nil {
177 return material, operator.ErrRefused
178 }
179 browserRaw, err := exchangeJSON("HOME_ASSERTION_KEYS", 4096)
180 var encoded map[string]string
181 if err != nil || json.Unmarshal([]byte(browserRaw), &encoded) != nil || len(encoded) == 0 || len(encoded) > 8 {
182 return material, operator.ErrRefused
183 }
184 browser := map[string][]byte{}
185 material.OtherKeys = [][]byte{ownerPub, signer.SignPub()}
186 for id, value := range encoded {
187 key, err := base64.StdEncoding.Strict().DecodeString(value)
188 if err != nil || base64.StdEncoding.EncodeToString(key) != value {
189 return material, operator.ErrRefused
190 }
191 browser[id] = key
192 material.OtherKeys = append(material.OtherKeys, key)
193 }
194 if _, err := assertion.NewVerifier(origins.Credential, origins.Directory, browser); err != nil {
195 return material, operator.ErrRefused
196 }
197 rosterRuntime, err := loadRosterRuntime(role, browser)
198 if err != nil {
199 return material, operator.ErrRefused
200 }
201 ids := map[string]bool{}
202 matched := rosterRuntime.mode != "serve"
203 for _, key := range cfg.Roster {
204 pub, err := operatorPublic(key.Public)
205 sum := sha256.Sum256(pub)
206 if err != nil || key.ID != hex.EncodeToString(sum[:]) || ids[key.ID] {
207 return material, operator.ErrRefused
208 }
209 ids[key.ID] = true
210 material.OtherKeys = append(material.OtherKeys, pub)
211 if rosterRuntime.mode == "serve" && bytes.Equal(pub, rosterRuntime.signer.SignPub()) {
212 matched = true
213 }
214 }
215 if !matched {
216 return material, operator.ErrRefused
217 }
218 var apps []string
219 for _, app := range directoryApps() {
220 if app.integrated {
221 apps = append(apps, app.slug)
222 }
223 }
224 policy := directorydata.Config{Apps: apps, ValidateRoute: directoryRouteAllowed}
225 material.Origin = origins.Directory
226 material.Build = func(engine *operator.Engine) (operator.CommandExecutor, error) {
227 return operator.NewDirectory(engine, owner, signer, cfg.Roster, policy)
228 }
229 }
230 if !distinctOperatorKeys(material.OtherKeys) {
231 return material, operator.ErrRefused
232 }
233 return material, nil
234}
235