carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package hostfloor
2
3import (
4 "context"
5 "crypto/ed25519"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "github.com/carlosframework/platform/internal/release"
11 "io"
12)
13
14const Key = "control/moves-protocol.json"
15const AuthKey = "control/moves-protocol-auth.json"
16
17var ErrFloor = errors.New("hostfloor: authenticated floor required")
18
19type Floor struct {
20 Min int `json:"min"`
21 MinRelease string `json:"min_release"`
22}
23type Authorization struct {
24 Schema int `json:"schema"`
25 Purpose string `json:"purpose"`
26 DeploymentBucket string `json:"deployment_bucket"`
27 Key string `json:"key"`
28 SHA256 string `json:"sha256"`
29}
30type Policy struct {
31 Schema int `json:"schema"`
32 DeploymentBucket string `json:"deployment_bucket"`
33 Mode string `json:"mode"`
34 Floor json.RawMessage `json:"floor"`
35 Authorization json.RawMessage `json:"authorization"`
36}
37type Verified struct {
38 Floor Floor
39 Digest string
40}
41
42func Digest(b []byte) string { sum := sha256.Sum256(b); return hex.EncodeToString(sum[:]) }
43
44// DigestReader is Digest over a stream, so an executable is hashed without
45// holding it in memory (the update unit runs under MemoryMax=256M).
46func DigestReader(r io.Reader) (string, error) {
47 h := sha256.New()
48 if _, err := io.Copy(h, r); err != nil {
49 return "", err
50 }
51 return hex.EncodeToString(h.Sum(nil)), nil
52}
53
54func Verify(pub ed25519.PublicKey, bucket string, raw, auth []byte) (Verified, error) {
55 var f Floor
56 var a Authorization
57 if bucket == "" || json.Unmarshal(raw, &f) != nil || f.Min < 1 {
58 return Verified{}, ErrFloor
59 }
60 if _, ok := release.ReleaseTag(f.MinRelease); !ok {
61 return Verified{}, ErrFloor
62 }
63 if release.VerifyRootEnvelope(pub, auth, &a) != nil || a.Schema != 1 || a.Purpose != "moves-protocol" || a.Key != Key || a.DeploymentBucket != bucket || a.SHA256 != Digest(raw) {
64 return Verified{}, ErrFloor
65 }
66 var fields map[string]json.RawMessage
67 if json.Unmarshal(raw, &fields) != nil || len(fields) != 2 {
68 return Verified{}, ErrFloor
69 }
70 return Verified{f, Digest(raw)}, nil
71}
72func Read(ctx context.Context, s release.Store, pub ed25519.PublicKey, bucket string) (Verified, error) {
73 raw, _, err := s.Get(ctx, Key)
74 if err != nil {
75 return Verified{}, err
76 }
77 auth, _, err := s.Get(ctx, AuthKey)
78 if err != nil {
79 return Verified{}, err
80 }
81 return Verify(pub, bucket, raw, auth)
82}
83
84// Authorize returns the exact floor bytes and the root-signed moves-protocol
85// authorization over them, verified before they are returned. It is the one
86// signer of that pair: Publish writes what it returns, and ArmedPolicy puts
87// the same bytes in the policy a box arms with before publication. ed25519
88// is deterministic and a Floor marshals one way, so the same key, bucket and
89// floor always give the same bytes, and a box armed offline holds exactly
90// the pair that is published later.
91func Authorize(sg release.Signer, bucket string, f Floor) (raw, auth []byte, err error) {
92 raw, err = json.Marshal(f)
93 if err != nil {
94 return nil, nil, err
95 }
96 auth, err = release.SignRootEnvelope(sg, Authorization{1, "moves-protocol", bucket, Key, Digest(raw)})
97 if err != nil {
98 return nil, nil, err
99 }
100 if _, err = Verify(sg.RootPublicKey(), bucket, raw, auth); err != nil {
101 return nil, nil, err
102 }
103 return raw, auth, nil
104}
105
106// ArmedPolicy is the policy guard-arm takes: the floor and its
107// authorization from Authorize, signed on the trusted host and written
108// nowhere. Publishing the floor is a separate, later step.
109func ArmedPolicy(sg release.Signer, bucket string, f Floor) (Policy, error) {
110 raw, auth, err := Authorize(sg, bucket, f)
111 if err != nil {
112 return Policy{}, err
113 }
114 p := Policy{Schema: 1, DeploymentBucket: bucket, Mode: "armed", Floor: raw, Authorization: auth}
115 if _, err = p.Verify(sg.RootPublicKey(), bucket); err != nil {
116 return Policy{}, err
117 }
118 return p, nil
119}
120
121func Publish(ctx context.Context, s release.Store, sg release.Signer, bucket string, f Floor) error {
122 raw, auth, err := Authorize(sg, bucket, f)
123 if err != nil {
124 return err
125 }
126 if err = release.CreateExact(ctx, s, AuthKey, auth); err != nil {
127 return err
128 }
129 return release.CreateExact(ctx, s, Key, raw)
130}
131func (p Policy) Verify(pub ed25519.PublicKey, bucket string) (Verified, error) {
132 if p.Schema != 1 || p.DeploymentBucket != bucket {
133 return Verified{}, ErrFloor
134 }
135 switch p.Mode {
136 case "unpublished":
137 if (len(p.Floor) != 0 && string(p.Floor) != "null") || (len(p.Authorization) != 0 && string(p.Authorization) != "null") {
138 return Verified{}, ErrFloor
139 }
140 return Verified{}, nil
141 case "armed", "enforced":
142 return Verify(pub, bucket, p.Floor, p.Authorization)
143 default:
144 return Verified{}, ErrFloor
145 }
146}
147func CheckImage(pub ed25519.PublicKey, f Verified, identity []byte, build, platform, hash string) error {
148 return CheckComponent(pub, f, identity, build, platform, hash, "router")
149}
150func CheckComponent(pub ed25519.PublicKey, f Verified, identity []byte, build, platform, hash, component string) error {
151 p, err := release.VerifySystemIdentity(pub, identity)
152 if err != nil {
153 return err
154 }
155 have, ok := release.ReleaseTag(p.Current.ReleaseTag)
156 if !ok {
157 return ErrFloor
158 }
159 want, ok := release.ReleaseTag(f.Floor.MinRelease)
160 if !ok {
161 return ErrFloor
162 }
163 a, ok := p.Current.Artifacts[platform]
164 if component == "console" {
165 a, ok = p.Current.ConsoleArtifacts[platform]
166 } else if component != "router" {
167 return ErrFloor
168 }
169 if !ok || build != p.Current.Version || hash != a.SHA256 || have.Compare(want) < 0 || p.Current.MovesProtocol < f.Floor.Min {
170 return ErrFloor
171 }
172 return nil
173}
174