oficina / home Public

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

Plain git — no account needed to clone.

Download

Download this file

1package home
2
3import (
4 "crypto/elliptic"
5 "crypto/sha256"
6 "encoding/base64"
7 "encoding/hex"
8 "errors"
9 "log/slog"
10 "os"
11
12 "amadan.net/oficina/home/handoff"
13 "amadan.net/oficina/home/internal/home/directorydata"
14 "amadan.net/oficina/home/internal/operator"
15 "amadan.net/rastrillo/rastrillo/assertion"
16 assertioncrypto "amadan.net/rastrillo/rastrillo/crypto"
17 "amadan.net/rastrillo/rastrillo/db"
18 "amadan.net/rastrillo/rastrillo/migrate"
19 "amadan.net/rastrillo/rastrillo/sessions"
20 json "github.com/go-json-experiment/json"
21)
22
23var errExchangeConfig = errors.New("home: invalid assertion configuration")
24var directorySchema = migrate.Merge(sessions.Schema, migrate.MustFromFS(handoff.Migrations, "oficina_handoff"), directorydata.Schema, directorydata.RosterSchema)
25
26type Runtime struct {
27 docsPreviewOrigin string
28 appPreviewOrigins map[string]string
29 roster rosterRuntime
30 role string
31 origins Origins
32 secrets Secrets
33 signer *assertioncrypto.Keypair
34 keyID string
35 keys map[string][]byte
36 consumers map[string]string
37}
38
39// LoadRuntime keeps existing deployments on the legacy path until a role is
40// explicitly configured. Directory boot never reads credential secrets.
41func LoadRuntime(dbPath string, logger *slog.Logger) (Runtime, error) {
42 var cfg Runtime
43 var err error
44 cfg.origins, err = LoadOrigins()
45 if err != nil {
46 return Runtime{}, err
47 }
48 cfg.role = os.Getenv("HOME_ROLE")
49 cfg.docsPreviewOrigin = os.Getenv("HOME_DOCS_PREVIEW_ORIGIN")
50 rawPreviews, err := exchangeJSON("HOME_APP_PREVIEW_ORIGINS", 8192)
51 if err != nil {
52 return Runtime{}, err
53 }
54 if rawPreviews != "" {
55 if json.Unmarshal([]byte(rawPreviews), &cfg.appPreviewOrigins) != nil || cfg.appPreviewOrigins == nil || len(cfg.appPreviewOrigins) == 0 || cfg.role != "directory" {
56 return Runtime{}, errExchangeConfig
57 }
58 }
59
60 if !cfg.validAppPreviews() {
61 return Runtime{}, errExchangeConfig
62 }
63 if cfg.role != "directory" {
64 cfg.roster, err = loadRosterRuntime(cfg.role, nil)
65 if err != nil {
66 return Runtime{}, err
67 }
68 }
69 switch cfg.role {
70 case "":
71 cfg.secrets, err = LoadSecrets(dbPath, logger)
72 return cfg, err
73 case "credential":
74 cfg.secrets, err = LoadSecrets(dbPath, logger)
75 if err != nil {
76 return Runtime{}, err
77 }
78 encodedPrivate := os.Getenv("HOME_ASSERTION_KEY")
79 if len(encodedPrivate) > 4096 {
80 return Runtime{}, errExchangeConfig
81 }
82 raw, err := base64.StdEncoding.DecodeString(encodedPrivate)
83 if err != nil {
84 return Runtime{}, errExchangeConfig
85 }
86 cfg.signer, err = assertioncrypto.UnmarshalKeypair(raw)
87 if err != nil {
88 return Runtime{}, errExchangeConfig
89 }
90 if cfg.signer.SignPriv.D.Sign() <= 0 || cfg.signer.SignPriv.D.Cmp(elliptic.P256().Params().N) >= 0 {
91 return Runtime{}, errExchangeConfig
92 }
93 pub := cfg.signer.SignPub()
94 sum := sha256.Sum256(pub)
95 cfg.keyID = hex.EncodeToString(sum[:])
96 cfg.keys = map[string][]byte{cfg.keyID: pub}
97 rawManifest, err := exchangeJSON("HOME_CONSUMERS", 8192)
98 if err != nil {
99 return Runtime{}, err
100 }
101 // Sixteen pilot origins keep the exact CSP manifest within ordinary header
102 // limits. Production registration needs a separately reviewed scalable path.
103 if len(rawManifest) > 8192 || json.Unmarshal([]byte(rawManifest), &cfg.consumers) != nil || len(cfg.consumers) == 0 || len(cfg.consumers) > 16 {
104 return Runtime{}, errExchangeConfig
105 }
106 for id, origin := range cfg.consumers {
107 if !consumerName(id) {
108 return Runtime{}, errExchangeConfig
109 }
110 if _, err = assertion.NewVerifier(cfg.origins.Credential, origin, cfg.keys); err != nil {
111 return Runtime{}, errExchangeConfig
112 }
113 }
114 if cfg.consumers["directory"] != cfg.origins.Directory {
115 return Runtime{}, errExchangeConfig
116 }
117 case "directory":
118 if dbPath == "" || os.Getenv("HOME_ASSERTION_KEY") != "" {
119 return Runtime{}, errExchangeConfig
120 }
121 cfg.secrets.Sealer, err = LoadSealer()
122 if err != nil {
123 return Runtime{}, err
124 }
125 var encoded map[string]string
126 raw, err := exchangeJSON("HOME_ASSERTION_KEYS", 4096)
127 if err != nil {
128 return Runtime{}, err
129 }
130 if len(raw) > 4096 || json.Unmarshal([]byte(raw), &encoded) != nil || len(encoded) == 0 || len(encoded) > 8 {
131 return Runtime{}, errExchangeConfig
132 }
133 cfg.keys = make(map[string][]byte, len(encoded))
134 for kid, value := range encoded {
135 key, err := base64.StdEncoding.DecodeString(value)
136 if err != nil {
137 return Runtime{}, errExchangeConfig
138 }
139 cfg.keys[kid] = key
140 }
141 if _, err = assertion.NewVerifier(cfg.origins.Credential, cfg.origins.Directory, cfg.keys); err != nil {
142 return Runtime{}, errExchangeConfig
143 }
144 cfg.roster, err = loadRosterRuntime(cfg.role, cfg.keys)
145 if err != nil {
146 return Runtime{}, err
147 }
148 default:
149 return Runtime{}, errExchangeConfig
150 }
151 return cfg, nil
152}
153func consumerName(s string) bool {
154 if len(s) == 0 || len(s) > 64 {
155 return false
156 }
157 for _, b := range []byte(s) {
158 if !(b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || b == '_' || b == '-') {
159 return false
160 }
161 }
162 return true
163}
164func (cfg Runtime) Schema() *migrate.Set {
165 if cfg.role == "directory" {
166 return migrate.Merge(directorySchema, directorydata.OperatorSchema, operator.DirectorySchema)
167 }
168 if cfg.role == "credential" {
169 return migrate.Merge(BootSchema, operator.CredentialSchema)
170 }
171 return BootSchema
172}
173func NewRuntime(cfg Runtime, d *db.DB, logger *slog.Logger) (*App, error) {
174 if !cfg.roster.valid(cfg.role, cfg.keys) || !cfg.validAppPreviews() {
175 return nil, errExchangeConfig
176 }
177 if cfg.role == "directory" {
178 return newDirectory(cfg, d, logger)
179 }
180 a, err := NewWithOrigins(cfg.secrets, d, logger, cfg.origins)
181 if err != nil {
182 return nil, err
183 }
184 if cfg.role != "" && cfg.role != "credential" {
185 return nil, errExchangeConfig
186 }
187 if cfg.role == "credential" && (cfg.signer == nil || cfg.consumers["directory"] != cfg.origins.Directory) {
188 return nil, errExchangeConfig
189 }
190 a.runtime = &cfg
191 return a, nil
192}
193
194// exchangeJSON accepts an explicitly encoded alternative for platforms whose
195// environment-file format cannot carry JSON quotes. Both forms together fail.
196func exchangeJSON(name string, limit int) (string, error) {
197 raw, encoded := os.Getenv(name), os.Getenv(name+"_BASE64")
198 if encoded == "" {
199 if len(raw) > limit {
200 return "", errExchangeConfig
201 }
202 return raw, nil
203 }
204 if raw != "" || len(encoded) > base64.StdEncoding.EncodedLen(limit) {
205 return "", errExchangeConfig
206 }
207 b, err := base64.StdEncoding.Strict().DecodeString(encoded)
208 if err != nil || len(b) > limit || base64.StdEncoding.EncodeToString(b) != encoded {
209 return "", errExchangeConfig
210 }
211 return string(b), nil
212}
213