{"action":"created","comment":{"url":"https://api.github.com/repos/flynn/go-tuf/pulls/comments/22624937","id":22624937,"diff_hunk":"@@ -1,71 +1,394 @@\n package client\n \n import (\n+\t\"bytes\"\n+\t\"encoding/json\"\n \t\"errors\"\n \t\"io\"\n+\t\"io/ioutil\"\n \t\"time\"\n \n-\t\"github.com/flynn/go-tuf\"\n \t\"github.com/flynn/go-tuf/data\"\n+\t\"github.com/flynn/go-tuf/keys\"\n+\t\"github.com/flynn/go-tuf/signed\"\n+\t\"github.com/flynn/go-tuf/util\"\n )\n \n var (\n-\tErrNotFound   = errors.New(\"tuf: file not found\")\n-\tErrLatest     = errors.New(\"tuf: the current version is the latest\")\n-\tErrWrongSize  = errors.New(\"tuf: unexpected file size\")\n-\tErrNoRootKeys = errors.New(\"tuf: no root keys found in local meta store\")\n+\tErrNotFound       = errors.New(\"tuf: file not found\")\n+\tErrLatest         = errors.New(\"tuf: the current version is the latest\")\n+\tErrWrongSize      = errors.New(\"tuf: unexpected file size\")\n+\tErrNoRootKeys     = errors.New(\"tuf: no root keys found in local meta store\")\n+\tErrChecksumFailed = errors.New(\"tuf: checksum failed\")\n )\n \n+// LocalStore is local storage for downloaded top-level metadata.\n+type LocalStore interface {\n+\t// GetMeta returns top-level metadata from local storage. The keys are\n+\t// in the form `ROLE.json`, with ROLE being a valid top-level role.\n+\tGetMeta() (map[string]json.RawMessage, error)\n+\n+\t// SetMeta persists the given top-level metadata in local storage, the\n+\t// name taking the same format as the keys returned by GetMeta.\n+\tSetMeta(name string, meta json.RawMessage) error\n+}\n+\n+// RemoteStore downloads top-level metadata and target files from a remote\n+// repository.\n type RemoteStore interface {\n-\tGet(name string, size int64) (io.ReadCloser, error)\n+\t// Get downloads the given file from remote storage.\n+\t//\n+\t// `file` should be relative to the root of remote repository (e.g.\n+\t// \"root.json\" or \"targets/path/to/target/file.txt\")\n+\tGet(file string, size int64) (io.ReadCloser, error)\n }\n \n+// Client provides methods for fetching updates from a remote repository and\n+// downloading remote target files.\n type Client struct {\n-\trepo   *tuf.Repo\n+\tlocal  LocalStore\n \tremote RemoteStore\n+\n+\t// The following four fields represent decoded metatdata from\n+\t// local storage\n+\troot      *data.Root\n+\ttargets   *data.Targets\n+\tsnapshot  *data.Snapshot\n+\ttimestamp *data.Timestamp\n+\n+\t// localMeta is the raw metadata from local storage and is used to\n+\t// check whether remote metadata is present locally\n+\tlocalMeta map[string]json.RawMessage\n+\n+\t// db is a key DB used for verifying metadata\n+\tdb *keys.DB\n }\n \n-// TODO: Client needs root keys\n-func NewClient(local tuf.LocalStore, remote RemoteStore) (*Client, error) {\n-\trepo, err := tuf.NewRepo(local)\n+func NewClient(local LocalStore, remote RemoteStore) *Client {\n+\treturn &Client{\n+\t\tlocal:  local,\n+\t\tremote: remote,\n+\t}\n+}\n+\n+// Init initializes a local repository.\n+//\n+// The latest root.json is fetched from remote storage, verified using rootKeys\n+// and threshold, and then saved in local storage. It is expected that rootKeys\n+// were securely distributed with the software being updated.\n+func (c *Client) Init(rootKeys []*data.Key, threshold int) error {\n+\trootJSON, err := c.downloadMeta(\"root.json\", nil)\n \tif err != nil {\n-\t\treturn nil, err\n+\t\treturn err\n+\t}\n+\n+\tc.db = keys.NewDB()\n+\trootKeyIDs := make([]string, len(rootKeys))\n+\tfor i, key := range rootKeys {\n+\t\tid := key.ID()\n+\t\trootKeyIDs[i] = id\n+\t\tif err := c.db.AddKey(id, key); err != nil {\n+\t\t\treturn err\n+\t\t}\n \t}\n-\treturn &Client{repo, remote}, nil\n+\trole := &data.Role{Threshold: threshold, KeyIDs: rootKeyIDs}\n+\tif err := c.db.AddRole(\"root\", role); err != nil {\n+\t\treturn err\n+\t}\n+\n+\tif err := c.verifyRoot(rootJSON); err != nil {\n+\t\treturn err\n+\t}\n+\n+\treturn c.local.SetMeta(\"root.json\", rootJSON)\n }\n \n-func (c *Client) Update() error {\n-\t// check for root keys\n+// Update downloads and verifies remote metadata and returns updated targets.\n+//\n+// It performs the update part of \"The client application\" workflow from\n+// section 5.1 of the TUF spec:\n+//\n+// https://github.com/theupdateframework/tuf/blob/v0.9.9/docs/tuf-spec.txt#L714\n+func (c *Client) Update() (data.Files, error) {\n+\t// Always start the update using local metadata\n+\tif err := c.getLocalMeta(); err != nil {\n+\t\treturn nil, err\n+\t}\n \n-\t// if no meta, get root.json\n-\t// if meta, get timestamp.json\n-\t// if new, get snapshot.json\n-\t// if new root.json, restart\n-\t// if new targets.json update\n-\t// fully check all signatures\n+\t// TODO: If we get an invalid signature downloading timestamp.json\n+\t//       or snapshot.json, download root.json and start again.\n \n-\t/*\n-\t\tIf at any point in the following process there is a problem (e.g., only expired\n-\t\tmetadata can be retrieved), the Root file is downloaded and the process starts\n-\t\tover. Optionally, the software update system using the framework can decide how\n-\t\tto proceed rather than automatically downloading a new Root file.\n+\t// Get timestamp.json, extract snapshot.json file meta and save the\n+\t// timestamp.json locally\n+\ttimestampJSON, err := c.downloadMeta(\"timestamp.json\", nil)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\tsnapshotMeta, err := c.decodeTimestamp(timestampJSON)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\tif err := c.local.SetMeta(\"timestamp.json\", timestampJSON); err != nil {\n+\t\treturn nil, err\n+\t}\n \n-\t\t\tTUF downloads and verifies timestamp.json.\n+\t// Return ErrLatest if we already have the latest snapshot.json\n+\tif c.hasMeta(\"snapshot.json\", snapshotMeta) {\n+\t\treturn nil, ErrLatest\n+\t}\n \n-\t\t\tIf timestamp.json indicates that snapshot.json has changed, TUF downloads\n-\t\t\tand verifies snapshot.json.\n+\t// Get snapshot.json, then extract root.json and targets.json file meta.\n+\t//\n+\t// The snapshot.json is only saved locally after checking root.json and\n+\t// targets.json so that it will be re-downloaded on subsequent updates\n+\t// if this update fails.\n+\tsnapshotJSON, err := c.downloadMeta(\"snapshot.json\", &snapshotMeta)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\trootMeta, targetsMeta, err := c.decodeSnapshot(snapshotJSON)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n \n-\t\t\tTUF determines which metadata files listed in snapshot.json differ from\n-\t\t\tthose described in the last snapshot.json that TUF has seen. If root.json\n-\t\t\thas changed, the update process starts over using the new root.json.\n+\t// If we don't have the root.json, download it, save it in local\n+\t// storage and restart the update\n+\tif !c.hasMeta(\"root.json\", rootMeta) {\n+\t\trootJSON, err := c.downloadMeta(\"root.json\", &rootMeta)\n+\t\tif err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\t\tif err := c.verifyRoot(rootJSON); err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\t\tif err := c.local.SetMeta(\"root.json\", rootJSON); err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\t\treturn c.Update()\n+\t}\n \n-\t\t\tTUF provides the software update system with a list of available files\n-\t\t\taccording to targets.json.\n-\t*/\n+\t// If we don't have the targets.json, download it, determine updated\n+\t// targets and save targets.json in local storage\n+\tvar updatedTargets data.Files\n+\tif !c.hasMeta(\"targets.json\", targetsMeta) {\n+\t\ttargetsJSON, err := c.downloadMeta(\"targets.json\", &targetsMeta)\n+\t\tif err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\t\tupdatedTargets, err = c.decodeTargets(targetsJSON)\n+\t\tif err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\t\tif err := c.local.SetMeta(\"targets.json\", targetsJSON); err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\t}\n+\n+\t// Save the snapshot.json now it has been processed successfully\n+\tif err := c.local.SetMeta(\"snapshot.json\", snapshotJSON); err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\treturn updatedTargets, nil\n+}\n+\n+// getLocalMeta decodes and verifies metadata from local storage.\n+//\n+// The verification of local files is purely for consistency, if an attacker\n+// has compromised the local storage, there is no guarantee it can be trusted.\n+func (c *Client) getLocalMeta() error {\n+\tmeta, err := c.local.GetMeta()\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tif rootJSON, ok := meta[\"root.json\"]; ok {\n+\t\t// unmarshal root.json without verifying as we need the root\n+\t\t// keys first\n+\t\ts := &data.Signed{}\n+\t\tif err := json.Unmarshal(rootJSON, s); err != nil {\n+\t\t\treturn err\n+\t\t}\n+\t\troot := &data.Root{}\n+\t\tif err := json.Unmarshal(s.Signed, root); err != nil {\n+\t\t\treturn err\n+\t\t}\n+\t\tdb := keys.NewDB()\n+\t\tfor id, k := range root.Keys {\n+\t\t\tif err := db.AddKey(id, k); err != nil {\n+\t\t\t\treturn err\n+\t\t\t}\n+\t\t}\n+\t\tfor name, role := range root.Roles {\n+\t\t\tif err := db.AddRole(name, role); err != nil {\n+\t\t\t\treturn err\n+\t\t\t}\n+\t\t}\n+\t\tif err := signed.Verify(s, \"root\", 0, db); err != nil {\n+\t\t\treturn err\n+\t\t}\n+\t\tc.root = root\n+\t\tc.db = db\n+\t} else {\n+\t\treturn ErrNoRootKeys\n+\t}\n \n+\tif snapshotJSON, ok := meta[\"snapshot.json\"]; ok {\n+\t\tsnapshot := &data.Snapshot{}\n+\t\tif err := signed.Unmarshal(snapshotJSON, snapshot, \"snapshot\", 0, c.db); err != nil {\n+\t\t\treturn err\n+\t\t}\n+\t\tc.snapshot = snapshot\n+\t}\n+\n+\tif targetsJSON, ok := meta[\"targets.json\"]; ok {\n+\t\ttargets := &data.Targets{}\n+\t\tif err := signed.Unmarshal(targetsJSON, targets, \"targets\", 0, c.db); err != nil {\n+\t\t\treturn err\n+\t\t}\n+\t\tc.targets = targets\n+\t}\n+\n+\tif timestampJSON, ok := meta[\"timestamp.json\"]; ok {\n+\t\ttimestamp := &data.Timestamp{}\n+\t\tif err := signed.Unmarshal(timestampJSON, timestamp, \"timestamp\", 0, c.db); err != nil {\n+\t\t\treturn err\n+\t\t}\n+\t\tc.timestamp = timestamp\n+\t}\n+\n+\tc.localMeta = meta\n \treturn nil\n }\n \n+// maxMetaSize is the maximum number of bytes that will be downloaded when\n+// getting remote metadata without knowning it's length.\n+const maxMetaSize = 50 * 1024\n+\n+// downloadMeta downloads top-level metadata from remote storage and verifies\n+// it using the given file metadata.\n+func (c *Client) downloadMeta(name string, m *data.FileMeta) ([]byte, error) {\n+\tvar size int64\n+\tif m != nil {\n+\t\tsize = m.Length\n+\t}\n+\tr, err := c.remote.Get(name, size)\n+\tif err != nil {\n+\t\tif err == ErrNotFound {\n+\t\t\treturn nil, ErrMissingRemoteMetadata{name}\n+\t\t}\n+\t\treturn nil, err\n+\t}\n+\tdefer r.Close()\n+\n+\t// if m is nil (e.g. when downloading timestamp.json, which has unknown\n+\t// size), just read the stream (upto a sane maximum) and return it","path":"client/client.go","position":319,"original_position":319,"commit_id":"0ae521abc2a5fa506f507a940d1f33d4cec3b818","original_commit_id":"0ae521abc2a5fa506f507a940d1f33d4cec3b818","user":{"login":"titanous","id":13026,"avatar_url":"https://avatars.githubusercontent.com/u/13026?v=3","gravatar_id":"","url":"https://api.github.com/users/titanous","html_url":"https://github.com/titanous","followers_url":"https://api.github.com/users/titanous/followers","following_url":"https://api.github.com/users/titanous/following{/other_user}","gists_url":"https://api.github.com/users/titanous/gists{/gist_id}","starred_url":"https://api.github.com/users/titanous/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/titanous/subscriptions","organizations_url":"https://api.github.com/users/titanous/orgs","repos_url":"https://api.github.com/users/titanous/repos","events_url":"https://api.github.com/users/titanous/events{/privacy}","received_events_url":"https://api.github.com/users/titanous/received_events","type":"User","site_admin":false},"body":"s/upto/up to/","created_at":"2015-01-07T23:20:36Z","updated_at":"2015-01-07T23:20:36Z","html_url":"https://github.com/flynn/go-tuf/pull/24#discussion_r22624937","pull_request_url":"https://api.github.com/repos/flynn/go-tuf/pulls/24","_links":{"self":{"href":"https://api.github.com/repos/flynn/go-tuf/pulls/comments/22624937"},"html":{"href":"https://github.com/flynn/go-tuf/pull/24#discussion_r22624937"},"pull_request":{"href":"https://api.github.com/repos/flynn/go-tuf/pulls/24"}}},"pull_request":{"url":"https://api.github.com/repos/flynn/go-tuf/pulls/24","id":26914231,"html_url":"https://github.com/flynn/go-tuf/pull/24","diff_url":"https://github.com/flynn/go-tuf/pull/24.diff","patch_url":"https://github.com/flynn/go-tuf/pull/24.patch","issue_url":"https://api.github.com/repos/flynn/go-tuf/issues/24","number":24,"state":"open","locked":false,"title":"Implement client.Update()","user":{"login":"lmars","id":488515,"avatar_url":"https://avatars.githubusercontent.com/u/488515?v=3","gravatar_id":"","url":"https://api.github.com/users/lmars","html_url":"https://github.com/lmars","followers_url":"https://api.github.com/users/lmars/followers","following_url":"https://api.github.com/users/lmars/following{/other_user}","gists_url":"https://api.github.com/users/lmars/gists{/gist_id}","starred_url":"https://api.github.com/users/lmars/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lmars/subscriptions","organizations_url":"https://api.github.com/users/lmars/orgs","repos_url":"https://api.github.com/users/lmars/repos","events_url":"https://api.github.com/users/lmars/events{/privacy}","received_events_url":"https://api.github.com/users/lmars/received_events","type":"User","site_admin":false},"body":"There are a couple of TODOs in here (i.e. handling invalid signatures, adding more tests) but I'll do them in supplemental PRs, I'm keen to get feedback on the implementation so far.","created_at":"2015-01-06T20:08:04Z","updated_at":"2015-01-07T23:20:36Z","closed_at":null,"merged_at":null,"merge_commit_sha":"9dd154aa59e6b53d61e70d19eac2f8f43b4544ac","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/flynn/go-tuf/pulls/24/commits","review_comments_url":"https://api.github.com/repos/flynn/go-tuf/pulls/24/comments","review_comment_url":"https://api.github.com/repos/flynn/go-tuf/pulls/comments/{number}","comments_url":"https://api.github.com/repos/flynn/go-tuf/issues/24/comments","statuses_url":"https://api.github.com/repos/flynn/go-tuf/statuses/0ae521abc2a5fa506f507a940d1f33d4cec3b818","head":{"label":"flynn:client-update","ref":"client-update","sha":"0ae521abc2a5fa506f507a940d1f33d4cec3b818","user":{"login":"flynn","id":4973613,"avatar_url":"https://avatars.githubusercontent.com/u/4973613?v=3","gravatar_id":"","url":"https://api.github.com/users/flynn","html_url":"https://github.com/flynn","followers_url":"https://api.github.com/users/flynn/followers","following_url":"https://api.github.com/users/flynn/following{/other_user}","gists_url":"https://api.github.com/users/flynn/gists{/gist_id}","starred_url":"https://api.github.com/users/flynn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/flynn/subscriptions","organizations_url":"https://api.github.com/users/flynn/orgs","repos_url":"https://api.github.com/users/flynn/repos","events_url":"https://api.github.com/users/flynn/events{/privacy}","received_events_url":"https://api.github.com/users/flynn/received_events","type":"Organization","site_admin":false},"repo":{"id":27903690,"name":"go-tuf","full_name":"flynn/go-tuf","owner":{"login":"flynn","id":4973613,"avatar_url":"https://avatars.githubusercontent.com/u/4973613?v=3","gravatar_id":"","url":"https://api.github.com/users/flynn","html_url":"https://github.com/flynn","followers_url":"https://api.github.com/users/flynn/followers","following_url":"https://api.github.com/users/flynn/following{/other_user}","gists_url":"https://api.github.com/users/flynn/gists{/gist_id}","starred_url":"https://api.github.com/users/flynn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/flynn/subscriptions","organizations_url":"https://api.github.com/users/flynn/orgs","repos_url":"https://api.github.com/users/flynn/repos","events_url":"https://api.github.com/users/flynn/events{/privacy}","received_events_url":"https://api.github.com/users/flynn/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/flynn/go-tuf","description":"WIP Go implementation of The Update Framework","fork":false,"url":"https://api.github.com/repos/flynn/go-tuf","forks_url":"https://api.github.com/repos/flynn/go-tuf/forks","keys_url":"https://api.github.com/repos/flynn/go-tuf/keys{/key_id}","collaborators_url":"https://api.github.com/repos/flynn/go-tuf/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/flynn/go-tuf/teams","hooks_url":"https://api.github.com/repos/flynn/go-tuf/hooks","issue_events_url":"https://api.github.com/repos/flynn/go-tuf/issues/events{/number}","events_url":"https://api.github.com/repos/flynn/go-tuf/events","assignees_url":"https://api.github.com/repos/flynn/go-tuf/assignees{/user}","branches_url":"https://api.github.com/repos/flynn/go-tuf/branches{/branch}","tags_url":"https://api.github.com/repos/flynn/go-tuf/tags","blobs_url":"https://api.github.com/repos/flynn/go-tuf/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/flynn/go-tuf/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/flynn/go-tuf/git/refs{/sha}","trees_url":"https://api.github.com/repos/flynn/go-tuf/git/trees{/sha}","statuses_url":"https://api.github.com/repos/flynn/go-tuf/statuses/{sha}","languages_url":"https://api.github.com/repos/flynn/go-tuf/languages","stargazers_url":"https://api.github.com/repos/flynn/go-tuf/stargazers","contributors_url":"https://api.github.com/repos/flynn/go-tuf/contributors","subscribers_url":"https://api.github.com/repos/flynn/go-tuf/subscribers","subscription_url":"https://api.github.com/repos/flynn/go-tuf/subscription","commits_url":"https://api.github.com/repos/flynn/go-tuf/commits{/sha}","git_commits_url":"https://api.github.com/repos/flynn/go-tuf/git/commits{/sha}","comments_url":"https://api.github.com/repos/flynn/go-tuf/comments{/number}","issue_comment_url":"https://api.github.com/repos/flynn/go-tuf/issues/comments/{number}","contents_url":"https://api.github.com/repos/flynn/go-tuf/contents/{+path}","compare_url":"https://api.github.com/repos/flynn/go-tuf/compare/{base}...{head}","merges_url":"https://api.github.com/repos/flynn/go-tuf/merges","archive_url":"https://api.github.com/repos/flynn/go-tuf/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/flynn/go-tuf/downloads","issues_url":"https://api.github.com/repos/flynn/go-tuf/issues{/number}","pulls_url":"https://api.github.com/repos/flynn/go-tuf/pulls{/number}","milestones_url":"https://api.github.com/repos/flynn/go-tuf/milestones{/number}","notifications_url":"https://api.github.com/repos/flynn/go-tuf/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/flynn/go-tuf/labels{/name}","releases_url":"https://api.github.com/repos/flynn/go-tuf/releases{/id}","created_at":"2014-12-12T03:31:16Z","updated_at":"2015-01-06T20:09:04Z","pushed_at":"2015-01-07T23:13:57Z","git_url":"git://github.com/flynn/go-tuf.git","ssh_url":"git@github.com:flynn/go-tuf.git","clone_url":"https://github.com/flynn/go-tuf.git","svn_url":"https://github.com/flynn/go-tuf","homepage":"http://theupdateframework.com/","size":336,"stargazers_count":39,"watchers_count":39,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"open_issues_count":13,"forks":1,"open_issues":13,"watchers":39,"default_branch":"master"}},"base":{"label":"flynn:master","ref":"master","sha":"c479ef0750ccd08e0e201c39f3c6ba96eb8c228a","user":{"login":"flynn","id":4973613,"avatar_url":"https://avatars.githubusercontent.com/u/4973613?v=3","gravatar_id":"","url":"https://api.github.com/users/flynn","html_url":"https://github.com/flynn","followers_url":"https://api.github.com/users/flynn/followers","following_url":"https://api.github.com/users/flynn/following{/other_user}","gists_url":"https://api.github.com/users/flynn/gists{/gist_id}","starred_url":"https://api.github.com/users/flynn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/flynn/subscriptions","organizations_url":"https://api.github.com/users/flynn/orgs","repos_url":"https://api.github.com/users/flynn/repos","events_url":"https://api.github.com/users/flynn/events{/privacy}","received_events_url":"https://api.github.com/users/flynn/received_events","type":"Organization","site_admin":false},"repo":{"id":27903690,"name":"go-tuf","full_name":"flynn/go-tuf","owner":{"login":"flynn","id":4973613,"avatar_url":"https://avatars.githubusercontent.com/u/4973613?v=3","gravatar_id":"","url":"https://api.github.com/users/flynn","html_url":"https://github.com/flynn","followers_url":"https://api.github.com/users/flynn/followers","following_url":"https://api.github.com/users/flynn/following{/other_user}","gists_url":"https://api.github.com/users/flynn/gists{/gist_id}","starred_url":"https://api.github.com/users/flynn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/flynn/subscriptions","organizations_url":"https://api.github.com/users/flynn/orgs","repos_url":"https://api.github.com/users/flynn/repos","events_url":"https://api.github.com/users/flynn/events{/privacy}","received_events_url":"https://api.github.com/users/flynn/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/flynn/go-tuf","description":"WIP Go implementation of The Update Framework","fork":false,"url":"https://api.github.com/repos/flynn/go-tuf","forks_url":"https://api.github.com/repos/flynn/go-tuf/forks","keys_url":"https://api.github.com/repos/flynn/go-tuf/keys{/key_id}","collaborators_url":"https://api.github.com/repos/flynn/go-tuf/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/flynn/go-tuf/teams","hooks_url":"https://api.github.com/repos/flynn/go-tuf/hooks","issue_events_url":"https://api.github.com/repos/flynn/go-tuf/issues/events{/number}","events_url":"https://api.github.com/repos/flynn/go-tuf/events","assignees_url":"https://api.github.com/repos/flynn/go-tuf/assignees{/user}","branches_url":"https://api.github.com/repos/flynn/go-tuf/branches{/branch}","tags_url":"https://api.github.com/repos/flynn/go-tuf/tags","blobs_url":"https://api.github.com/repos/flynn/go-tuf/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/flynn/go-tuf/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/flynn/go-tuf/git/refs{/sha}","trees_url":"https://api.github.com/repos/flynn/go-tuf/git/trees{/sha}","statuses_url":"https://api.github.com/repos/flynn/go-tuf/statuses/{sha}","languages_url":"https://api.github.com/repos/flynn/go-tuf/languages","stargazers_url":"https://api.github.com/repos/flynn/go-tuf/stargazers","contributors_url":"https://api.github.com/repos/flynn/go-tuf/contributors","subscribers_url":"https://api.github.com/repos/flynn/go-tuf/subscribers","subscription_url":"https://api.github.com/repos/flynn/go-tuf/subscription","commits_url":"https://api.github.com/repos/flynn/go-tuf/commits{/sha}","git_commits_url":"https://api.github.com/repos/flynn/go-tuf/git/commits{/sha}","comments_url":"https://api.github.com/repos/flynn/go-tuf/comments{/number}","issue_comment_url":"https://api.github.com/repos/flynn/go-tuf/issues/comments/{number}","contents_url":"https://api.github.com/repos/flynn/go-tuf/contents/{+path}","compare_url":"https://api.github.com/repos/flynn/go-tuf/compare/{base}...{head}","merges_url":"https://api.github.com/repos/flynn/go-tuf/merges","archive_url":"https://api.github.com/repos/flynn/go-tuf/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/flynn/go-tuf/downloads","issues_url":"https://api.github.com/repos/flynn/go-tuf/issues{/number}","pulls_url":"https://api.github.com/repos/flynn/go-tuf/pulls{/number}","milestones_url":"https://api.github.com/repos/flynn/go-tuf/milestones{/number}","notifications_url":"https://api.github.com/repos/flynn/go-tuf/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/flynn/go-tuf/labels{/name}","releases_url":"https://api.github.com/repos/flynn/go-tuf/releases{/id}","created_at":"2014-12-12T03:31:16Z","updated_at":"2015-01-06T20:09:04Z","pushed_at":"2015-01-07T23:13:57Z","git_url":"git://github.com/flynn/go-tuf.git","ssh_url":"git@github.com:flynn/go-tuf.git","clone_url":"https://github.com/flynn/go-tuf.git","svn_url":"https://github.com/flynn/go-tuf","homepage":"http://theupdateframework.com/","size":336,"stargazers_count":39,"watchers_count":39,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"open_issues_count":13,"forks":1,"open_issues":13,"watchers":39,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/flynn/go-tuf/pulls/24"},"html":{"href":"https://github.com/flynn/go-tuf/pull/24"},"issue":{"href":"https://api.github.com/repos/flynn/go-tuf/issues/24"},"comments":{"href":"https://api.github.com/repos/flynn/go-tuf/issues/24/comments"},"review_comments":{"href":"https://api.github.com/repos/flynn/go-tuf/pulls/24/comments"},"review_comment":{"href":"https://api.github.com/repos/flynn/go-tuf/pulls/comments/{number}"},"commits":{"href":"https://api.github.com/repos/flynn/go-tuf/pulls/24/commits"},"statuses":{"href":"https://api.github.com/repos/flynn/go-tuf/statuses/0ae521abc2a5fa506f507a940d1f33d4cec3b818"}}}}