carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package archive
2
3import (
4 "archive/tar"
5 "crypto/ecdh"
6 "crypto/sha256"
7 "errors"
8 "fmt"
9 "io"
10
11 "github.com/carlosframework/platform/internal/appconfig"
12)
13
14// errVerifyAborted is what Verify closes its pipe with when it stops
15// reading before the end, so Open's next write fails and its goroutine
16// exits instead of blocking on the pipe for the rest of the archive.
17var errVerifyAborted = errors.New("archive: verify stopped reading")
18
19// check validates a manifest on its own, before any byte of the archive is
20// read: the version and chunk size this package writes, the digests and
21// key present, and an entry list Build could have written (valid, unique
22// paths within the limits, sizes summing to Bytes, at most Cap).
23func (m Manifest) check() error {
24 switch {
25 case m.V != ManifestVersion:
26 return fmt.Errorf("archive: manifest version %d, want %d", m.V, ManifestVersion)
27 case m.Chunk != ChunkSize:
28 return fmt.Errorf("archive: manifest chunk %d, want %d", m.Chunk, ChunkSize)
29 case m.Plain == "" || m.Sealed == "" || m.SealedDataKey == "":
30 return errors.New("archive: manifest carries no sealed digest, plaintext digest or sealed data key")
31 case len(m.Entries) > entryLimit:
32 return ErrTooManyEntries
33 }
34 seen := make(map[string]bool, len(m.Entries))
35 var sum int64
36 for _, e := range m.Entries {
37 if _, _, err := checkName(e.Path, false); err != nil {
38 return err
39 }
40 if seen[e.Path] {
41 return fmt.Errorf("%w: %q named twice in the manifest", ErrBadPath, e.Path)
42 }
43 seen[e.Path] = true
44 if e.Size < 0 || e.Size > Cap-sum {
45 return ErrOverCap
46 }
47 sum += e.Size
48 }
49 if sum != m.Bytes {
50 return fmt.Errorf("archive: manifest entries hold %d bytes, manifest says %d", sum, m.Bytes)
51 }
52 return nil
53}
54
55// Verify is T5 (§7.3): it opens the sealed object with priv, streams the
56// tar out of zstd and checks everything the manifest promised: the
57// plaintext digest, every entry's path, size and sha256, the entry types
58// (regular files and directories only), the limits, and the sealed digest
59// of the bytes it read. It writes nothing and returns the first mismatch.
60// Open's own verdict (every chunk authenticated, the final chunk present
61// and last) wins over any mismatch found in what it produced.
62func Verify(sealed io.Reader, m Manifest, priv *ecdh.PrivateKey, b appconfig.Binding) error {
63 if err := m.check(); err != nil {
64 return err
65 }
66 sealedHash := sha256.New()
67 pr, pw := io.Pipe()
68 done := make(chan error, 1)
69 go func() {
70 err := Open(io.TeeReader(sealed, sealedHash), m.SealedDataKey, priv, b, pw)
71 pw.CloseWithError(err)
72 done <- err
73 }()
74 plain, err := verifyStream(pr, m)
75 // Break the pipe whatever happened, so a producer still writing fails
76 // and exits, then wait for it. After a clean read to EOF this is a
77 // no-op: Open has already returned.
78 pr.CloseWithError(errVerifyAborted)
79 if openErr := <-done; openErr != nil && !errors.Is(openErr, errVerifyAborted) {
80 return openErr
81 }
82 if err != nil {
83 return err
84 }
85 if plain != m.Plain {
86 return fmt.Errorf("archive: plaintext digest %s, manifest says %s", plain, m.Plain)
87 }
88 if got := digestString(sealedHash); got != m.Sealed {
89 return fmt.Errorf("archive: sealed digest %s, manifest says %s", got, m.Sealed)
90 }
91 return nil
92}
93
94// verifyStream reads the opened archive from r to its end and checks it
95// against m, returning the plaintext digest. h hashes the decoded tar,
96// end marker included, which is exactly what Build's hashWriter hashed (it
97// sits between tar.Writer and the compressor), so Plain is the same bytes
98// on both sides (round-4 finding 8).
99func verifyStream(r io.Reader, m Manifest) (string, error) {
100 zr, err := newDecoder(r)
101 if err != nil {
102 return "", err
103 }
104 defer zr.Close()
105 h := sha256.New()
106 dec := &boundedReader{r: zr, left: decodedLimit}
107 tr := tar.NewReader(io.TeeReader(dec, h))
108 want := make(map[string]Entry, len(m.Entries))
109 for _, e := range m.Entries {
110 want[e.Path] = e
111 }
112 rules := newStreamRules()
113 for {
114 hdr, err := tr.Next()
115 if err == io.EOF {
116 break
117 }
118 if err != nil {
119 return "", err
120 }
121 name, _, _, err := rules.admit(hdr)
122 if err != nil {
123 return "", err
124 }
125 if hdr.Typeflag == tar.TypeDir {
126 continue
127 }
128 e, ok := want[name]
129 if !ok {
130 return "", fmt.Errorf("archive: entry %q is not in the manifest", hdr.Name)
131 }
132 if e.Size != hdr.Size {
133 return "", fmt.Errorf("archive: entry %q size differs from the manifest", hdr.Name)
134 }
135 fh := sha256.New()
136 if _, err := io.CopyN(fh, tr, hdr.Size); err != nil {
137 return "", err
138 }
139 if digestString(fh) != e.SHA256 {
140 return "", fmt.Errorf("archive: entry %q digest differs", hdr.Name)
141 }
142 delete(want, name)
143 }
144 // tar.Reader stops at the end-of-archive marker; whatever follows in
145 // the decoded stream must still reach h, and the raw stream must then
146 // end, so Open's terminal result becomes this read's error. A Verify
147 // that returned at the tar marker would trust an archive whose tail
148 // was never authenticated.
149 if _, err := io.Copy(h, dec); err != nil {
150 return "", err
151 }
152 if err := requireEOF(r); err != nil {
153 return "", err
154 }
155 if len(want) != 0 {
156 return "", fmt.Errorf("archive: %d manifest entries missing from the archive", len(want))
157 }
158 if rules.content != m.Bytes {
159 return "", fmt.Errorf("archive: the archive holds %d bytes, manifest says %d", rules.content, m.Bytes)
160 }
161 return digestString(h), nil
162}
163