package hostfloor import ( "context" "crypto/ed25519" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "github.com/carlosframework/platform/internal/release" "io" ) const Key = "control/moves-protocol.json" const AuthKey = "control/moves-protocol-auth.json" var ErrFloor = errors.New("hostfloor: authenticated floor required") type Floor struct { Min int `json:"min"` MinRelease string `json:"min_release"` } type Authorization struct { Schema int `json:"schema"` Purpose string `json:"purpose"` DeploymentBucket string `json:"deployment_bucket"` Key string `json:"key"` SHA256 string `json:"sha256"` } type Policy struct { Schema int `json:"schema"` DeploymentBucket string `json:"deployment_bucket"` Mode string `json:"mode"` Floor json.RawMessage `json:"floor"` Authorization json.RawMessage `json:"authorization"` } type Verified struct { Floor Floor Digest string } func Digest(b []byte) string { sum := sha256.Sum256(b); return hex.EncodeToString(sum[:]) } // DigestReader is Digest over a stream, so an executable is hashed without // holding it in memory (the update unit runs under MemoryMax=256M). func DigestReader(r io.Reader) (string, error) { h := sha256.New() if _, err := io.Copy(h, r); err != nil { return "", err } return hex.EncodeToString(h.Sum(nil)), nil } func Verify(pub ed25519.PublicKey, bucket string, raw, auth []byte) (Verified, error) { var f Floor var a Authorization if bucket == "" || json.Unmarshal(raw, &f) != nil || f.Min < 1 { return Verified{}, ErrFloor } if _, ok := release.ReleaseTag(f.MinRelease); !ok { return Verified{}, ErrFloor } if release.VerifyRootEnvelope(pub, auth, &a) != nil || a.Schema != 1 || a.Purpose != "moves-protocol" || a.Key != Key || a.DeploymentBucket != bucket || a.SHA256 != Digest(raw) { return Verified{}, ErrFloor } var fields map[string]json.RawMessage if json.Unmarshal(raw, &fields) != nil || len(fields) != 2 { return Verified{}, ErrFloor } return Verified{f, Digest(raw)}, nil } func Read(ctx context.Context, s release.Store, pub ed25519.PublicKey, bucket string) (Verified, error) { raw, _, err := s.Get(ctx, Key) if err != nil { return Verified{}, err } auth, _, err := s.Get(ctx, AuthKey) if err != nil { return Verified{}, err } return Verify(pub, bucket, raw, auth) } // Authorize returns the exact floor bytes and the root-signed moves-protocol // authorization over them, verified before they are returned. It is the one // signer of that pair: Publish writes what it returns, and ArmedPolicy puts // the same bytes in the policy a box arms with before publication. ed25519 // is deterministic and a Floor marshals one way, so the same key, bucket and // floor always give the same bytes, and a box armed offline holds exactly // the pair that is published later. func Authorize(sg release.Signer, bucket string, f Floor) (raw, auth []byte, err error) { raw, err = json.Marshal(f) if err != nil { return nil, nil, err } auth, err = release.SignRootEnvelope(sg, Authorization{1, "moves-protocol", bucket, Key, Digest(raw)}) if err != nil { return nil, nil, err } if _, err = Verify(sg.RootPublicKey(), bucket, raw, auth); err != nil { return nil, nil, err } return raw, auth, nil } // ArmedPolicy is the policy guard-arm takes: the floor and its // authorization from Authorize, signed on the trusted host and written // nowhere. Publishing the floor is a separate, later step. func ArmedPolicy(sg release.Signer, bucket string, f Floor) (Policy, error) { raw, auth, err := Authorize(sg, bucket, f) if err != nil { return Policy{}, err } p := Policy{Schema: 1, DeploymentBucket: bucket, Mode: "armed", Floor: raw, Authorization: auth} if _, err = p.Verify(sg.RootPublicKey(), bucket); err != nil { return Policy{}, err } return p, nil } func Publish(ctx context.Context, s release.Store, sg release.Signer, bucket string, f Floor) error { raw, auth, err := Authorize(sg, bucket, f) if err != nil { return err } if err = release.CreateExact(ctx, s, AuthKey, auth); err != nil { return err } return release.CreateExact(ctx, s, Key, raw) } func (p Policy) Verify(pub ed25519.PublicKey, bucket string) (Verified, error) { if p.Schema != 1 || p.DeploymentBucket != bucket { return Verified{}, ErrFloor } switch p.Mode { case "unpublished": if (len(p.Floor) != 0 && string(p.Floor) != "null") || (len(p.Authorization) != 0 && string(p.Authorization) != "null") { return Verified{}, ErrFloor } return Verified{}, nil case "armed", "enforced": return Verify(pub, bucket, p.Floor, p.Authorization) default: return Verified{}, ErrFloor } } func CheckImage(pub ed25519.PublicKey, f Verified, identity []byte, build, platform, hash string) error { return CheckComponent(pub, f, identity, build, platform, hash, "router") } func CheckComponent(pub ed25519.PublicKey, f Verified, identity []byte, build, platform, hash, component string) error { p, err := release.VerifySystemIdentity(pub, identity) if err != nil { return err } have, ok := release.ReleaseTag(p.Current.ReleaseTag) if !ok { return ErrFloor } want, ok := release.ReleaseTag(f.Floor.MinRelease) if !ok { return ErrFloor } a, ok := p.Current.Artifacts[platform] if component == "console" { a, ok = p.Current.ConsoleArtifacts[platform] } else if component != "router" { return ErrFloor } if !ok || build != p.Current.Version || hash != a.SHA256 || have.Compare(want) < 0 || p.Current.MovesProtocol < f.Floor.Min { return ErrFloor } return nil }