util.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. package util
  2. import (
  3. "bytes"
  4. "crypto/hmac"
  5. "crypto/sha256"
  6. "crypto/sha512"
  7. "encoding/hex"
  8. "encoding/json"
  9. "fmt"
  10. "hash"
  11. "io"
  12. "io/ioutil"
  13. "os"
  14. "path"
  15. "path/filepath"
  16. "strconv"
  17. "strings"
  18. "github.com/DataDog/go-tuf/data"
  19. )
  20. type ErrWrongLength struct {
  21. Expected int64
  22. Actual int64
  23. }
  24. func (e ErrWrongLength) Error() string {
  25. return fmt.Sprintf("wrong length, expected %d got %d", e.Expected, e.Actual)
  26. }
  27. type ErrWrongVersion struct {
  28. Expected int64
  29. Actual int64
  30. }
  31. func (e ErrWrongVersion) Error() string {
  32. return fmt.Sprintf("wrong version, expected %d got %d", e.Expected, e.Actual)
  33. }
  34. type ErrWrongHash struct {
  35. Type string
  36. Expected data.HexBytes
  37. Actual data.HexBytes
  38. }
  39. func (e ErrWrongHash) Error() string {
  40. return fmt.Sprintf("wrong %s hash, expected %s got %s", e.Type, hex.EncodeToString(e.Expected), hex.EncodeToString(e.Actual))
  41. }
  42. type ErrNoCommonHash struct {
  43. Expected data.Hashes
  44. Actual data.Hashes
  45. }
  46. func (e ErrNoCommonHash) Error() string {
  47. types := func(a data.Hashes) []string {
  48. t := make([]string, 0, len(a))
  49. for typ := range a {
  50. t = append(t, typ)
  51. }
  52. return t
  53. }
  54. return fmt.Sprintf("no common hash function, expected one of %s, got %s", types(e.Expected), types(e.Actual))
  55. }
  56. type ErrUnknownHashAlgorithm struct {
  57. Name string
  58. }
  59. func (e ErrUnknownHashAlgorithm) Error() string {
  60. return fmt.Sprintf("unknown hash algorithm: %s", e.Name)
  61. }
  62. type PassphraseFunc func(role string, confirm bool, change bool) ([]byte, error)
  63. func FileMetaEqual(actual data.FileMeta, expected data.FileMeta) error {
  64. if actual.Length != expected.Length {
  65. return ErrWrongLength{expected.Length, actual.Length}
  66. }
  67. if err := hashEqual(actual.Hashes, expected.Hashes); err != nil {
  68. return err
  69. }
  70. return nil
  71. }
  72. func hashEqual(actual data.Hashes, expected data.Hashes) error {
  73. hashChecked := false
  74. for typ, hash := range expected {
  75. if h, ok := actual[typ]; ok {
  76. hashChecked = true
  77. if !hmac.Equal(h, hash) {
  78. return ErrWrongHash{typ, hash, h}
  79. }
  80. }
  81. }
  82. if !hashChecked {
  83. return ErrNoCommonHash{expected, actual}
  84. }
  85. return nil
  86. }
  87. func versionEqual(actual int64, expected int64) error {
  88. if actual != expected {
  89. return ErrWrongVersion{expected, actual}
  90. }
  91. return nil
  92. }
  93. func SnapshotFileMetaEqual(actual data.SnapshotFileMeta, expected data.SnapshotFileMeta) error {
  94. // TUF-1.0 no longer considers the length and hashes to be a required
  95. // member of snapshots. However they are considering requiring hashes
  96. // for delegated roles to avoid an attack described in Section 5.6 of
  97. // the Mercury paper:
  98. // https://github.com/DataDog/specification/pull/40
  99. if expected.Length != 0 && actual.Length != expected.Length {
  100. return ErrWrongLength{expected.Length, actual.Length}
  101. }
  102. // 5.6.2 - Check against snapshot role's targets hash
  103. if len(expected.Hashes) != 0 {
  104. if err := hashEqual(actual.Hashes, expected.Hashes); err != nil {
  105. return err
  106. }
  107. }
  108. // 5.6.4 - Check against snapshot role's snapshot version
  109. if err := versionEqual(actual.Version, expected.Version); err != nil {
  110. return err
  111. }
  112. return nil
  113. }
  114. func TargetFileMetaEqual(actual data.TargetFileMeta, expected data.TargetFileMeta) error {
  115. return FileMetaEqual(actual.FileMeta, expected.FileMeta)
  116. }
  117. func TimestampFileMetaEqual(actual data.TimestampFileMeta, expected data.TimestampFileMeta) error {
  118. // TUF no longer considers the length and hashes to be a required
  119. // member of Timestamp.
  120. if expected.Length != 0 && actual.Length != expected.Length {
  121. return ErrWrongLength{expected.Length, actual.Length}
  122. }
  123. // 5.5.2 - Check against timestamp role's snapshot hash
  124. if len(expected.Hashes) != 0 {
  125. if err := hashEqual(actual.Hashes, expected.Hashes); err != nil {
  126. return err
  127. }
  128. }
  129. // 5.5.4 - Check against timestamp role's snapshot version
  130. if err := versionEqual(actual.Version, expected.Version); err != nil {
  131. return err
  132. }
  133. return nil
  134. }
  135. const defaultHashAlgorithm = "sha512"
  136. func GenerateFileMeta(r io.Reader, hashAlgorithms ...string) (data.FileMeta, error) {
  137. if len(hashAlgorithms) == 0 {
  138. hashAlgorithms = []string{defaultHashAlgorithm}
  139. }
  140. hashes := make(map[string]hash.Hash, len(hashAlgorithms))
  141. for _, hashAlgorithm := range hashAlgorithms {
  142. var h hash.Hash
  143. switch hashAlgorithm {
  144. case "sha256":
  145. h = sha256.New()
  146. case "sha512":
  147. h = sha512.New()
  148. default:
  149. return data.FileMeta{}, ErrUnknownHashAlgorithm{hashAlgorithm}
  150. }
  151. hashes[hashAlgorithm] = h
  152. r = io.TeeReader(r, h)
  153. }
  154. n, err := io.Copy(ioutil.Discard, r)
  155. if err != nil {
  156. return data.FileMeta{}, err
  157. }
  158. m := data.FileMeta{Length: n, Hashes: make(data.Hashes, len(hashes))}
  159. for hashAlgorithm, h := range hashes {
  160. m.Hashes[hashAlgorithm] = h.Sum(nil)
  161. }
  162. return m, nil
  163. }
  164. type versionedMeta struct {
  165. Version int64 `json:"version"`
  166. }
  167. func generateVersionedFileMeta(r io.Reader, hashAlgorithms ...string) (data.FileMeta, int64, error) {
  168. b, err := ioutil.ReadAll(r)
  169. if err != nil {
  170. return data.FileMeta{}, 0, err
  171. }
  172. m, err := GenerateFileMeta(bytes.NewReader(b), hashAlgorithms...)
  173. if err != nil {
  174. return data.FileMeta{}, 0, err
  175. }
  176. s := data.Signed{}
  177. if err := json.Unmarshal(b, &s); err != nil {
  178. return data.FileMeta{}, 0, err
  179. }
  180. vm := versionedMeta{}
  181. if err := json.Unmarshal(s.Signed, &vm); err != nil {
  182. return data.FileMeta{}, 0, err
  183. }
  184. return m, vm.Version, nil
  185. }
  186. func GenerateSnapshotFileMeta(r io.Reader, hashAlgorithms ...string) (data.SnapshotFileMeta, error) {
  187. m, v, err := generateVersionedFileMeta(r, hashAlgorithms...)
  188. if err != nil {
  189. return data.SnapshotFileMeta{}, err
  190. }
  191. return data.SnapshotFileMeta{
  192. FileMeta: m,
  193. Version: v,
  194. }, nil
  195. }
  196. func GenerateTargetFileMeta(r io.Reader, hashAlgorithms ...string) (data.TargetFileMeta, error) {
  197. m, err := GenerateFileMeta(r, hashAlgorithms...)
  198. if err != nil {
  199. return data.TargetFileMeta{}, err
  200. }
  201. return data.TargetFileMeta{
  202. FileMeta: m,
  203. }, nil
  204. }
  205. func GenerateTimestampFileMeta(r io.Reader, hashAlgorithms ...string) (data.TimestampFileMeta, error) {
  206. m, v, err := generateVersionedFileMeta(r, hashAlgorithms...)
  207. if err != nil {
  208. return data.TimestampFileMeta{}, err
  209. }
  210. return data.TimestampFileMeta{
  211. FileMeta: m,
  212. Version: v,
  213. }, nil
  214. }
  215. func NormalizeTarget(p string) string {
  216. // FIXME(TUF-0.9) TUF-1.0 is considering banning paths that begin with
  217. // a leading path separator, to avoid surprising behavior when joining
  218. // target and delgated paths. python-tuf raises an exception if any
  219. // path starts with '/', but since we need to be cross compatible with
  220. // TUF-0.9 we still need to support leading slashes. For now, we will
  221. // just strip them out, but eventually we should also consider turning
  222. // them into an error.
  223. return strings.TrimPrefix(path.Join("/", p), "/")
  224. }
  225. func VersionedPath(p string, version int64) string {
  226. return path.Join(path.Dir(p), strconv.FormatInt(version, 10)+"."+path.Base(p))
  227. }
  228. func HashedPaths(p string, hashes data.Hashes) []string {
  229. paths := make([]string, 0, len(hashes))
  230. for _, hash := range hashes {
  231. hashedPath := path.Join(path.Dir(p), hash.String()+"."+path.Base(p))
  232. paths = append(paths, hashedPath)
  233. }
  234. return paths
  235. }
  236. func AtomicallyWriteFile(filename string, data []byte, perm os.FileMode) error {
  237. dir, name := filepath.Split(filename)
  238. f, err := ioutil.TempFile(dir, name)
  239. if err != nil {
  240. return err
  241. }
  242. _, err = f.Write(data)
  243. if err != nil {
  244. f.Close()
  245. os.Remove(f.Name())
  246. return err
  247. }
  248. if err = f.Chmod(perm); err != nil {
  249. f.Close()
  250. os.Remove(f.Name())
  251. return err
  252. }
  253. if err := f.Close(); err != nil {
  254. os.Remove(f.Name())
  255. return err
  256. }
  257. if err := os.Rename(f.Name(), filename); err != nil {
  258. os.Remove(f.Name())
  259. return err
  260. }
  261. return nil
  262. }