{"action":"created","comment":{"url":"https://api.github.com/repos/letsencrypt/boulder/pulls/comments/39699868","id":39699868,"diff_hunk":"@@ -0,0 +1,264 @@\n+// Copyright 2015 ISRG.  All rights reserved\n+// This Source Code Form is subject to the terms of the Mozilla Public\n+// License, v. 2.0. If a copy of the MPL was not distributed with this\n+// file, You can obtain one at http://mozilla.org/MPL/2.0/.\n+\n+package publisher\n+\n+import (\n+\t\"bytes\"\n+\t\"crypto/ecdsa\"\n+\t\"crypto/sha256\"\n+\t\"crypto/x509\"\n+\t\"encoding/base64\"\n+\t\"encoding/json\"\n+\t\"fmt\"\n+\t\"io/ioutil\"\n+\t\"net/http\"\n+\t\"strconv\"\n+\t\"strings\"\n+\t\"time\"\n+\n+\t\"github.com/letsencrypt/boulder/core\"\n+\tblog \"github.com/letsencrypt/boulder/log\"\n+)\n+\n+type LogDescription struct {\n+\tID        string\n+\tURI       string\n+\tPublicKey *ecdsa.PublicKey\n+}\n+\n+type rawLogDescription struct {\n+\tURI       string `json:\"uri\"`\n+\tPublicKey string `json:\"key\"`\n+}\n+\n+func (logDesc *LogDescription) UnmarshalJSON(data []byte) error {\n+\tvar rawLogDesc rawLogDescription\n+\tif err := json.Unmarshal(data, &rawLogDesc); err != nil {\n+\t\treturn fmt.Errorf(\"Failed to unmarshal log description, %s\", err)\n+\t}\n+\tlogDesc.URI = rawLogDesc.URI\n+\t// Load Key\n+\tpkBytes, err := base64.StdEncoding.DecodeString(rawLogDesc.PublicKey)\n+\tif err != nil {\n+\t\treturn fmt.Errorf(\"\")\n+\t}\n+\tpk, err := x509.ParsePKIXPublicKey(pkBytes)\n+\tif err != nil {\n+\t\treturn fmt.Errorf(\"\")\n+\t}\n+\tecdsaKey, ok := pk.(*ecdsa.PublicKey)\n+\tif !ok {\n+\t\treturn fmt.Errorf(\"Failed to unmarshal log description for %s, unsupported public key type\", logDesc.URI)\n+\t}\n+\tlogDesc.PublicKey = ecdsaKey\n+\n+\t// Generate key hash for log ID\n+\tpkHash := sha256.Sum256(pkBytes)\n+\tlogDesc.ID = base64.StdEncoding.EncodeToString(pkHash[:])\n+\tif len(logDesc.ID) != 44 {\n+\t\treturn fmt.Errorf(\"Invalid log ID length [%d]\", len(logDesc.ID))\n+\t}\n+\n+\treturn nil\n+}\n+\n+// CTConfig defines the JSON configuration file schema\n+type CTConfig struct {\n+\tLogs              []LogDescription `json:\"logs\"`\n+\tSubmissionRetries int              `json:\"submissionRetries\"`\n+\t// This should use the same method as the DNS resolver\n+\tSubmissionBackoffString string        `json:\"submissionBackoff\"`\n+\tSubmissionBackoff       time.Duration `json:\"-\"`\n+\n+\tBundleFilename string   `json:\"intermediateBundleFilename\"`\n+\tIssuerBundle   []string `json:\"-\"`","path":"publisher/publisher.go","position":77,"original_position":77,"commit_id":"8fa508a376ce91d3b3da97e50d86451748180108","original_commit_id":"8fa508a376ce91d3b3da97e50d86451748180108","user":{"login":"jsha","id":220205,"avatar_url":"https://avatars.githubusercontent.com/u/220205?v=3","gravatar_id":"","url":"https://api.github.com/users/jsha","html_url":"https://github.com/jsha","followers_url":"https://api.github.com/users/jsha/followers","following_url":"https://api.github.com/users/jsha/following{/other_user}","gists_url":"https://api.github.com/users/jsha/gists{/gist_id}","starred_url":"https://api.github.com/users/jsha/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jsha/subscriptions","organizations_url":"https://api.github.com/users/jsha/orgs","repos_url":"https://api.github.com/users/jsha/repos","events_url":"https://api.github.com/users/jsha/events{/privacy}","received_events_url":"https://api.github.com/users/jsha/received_events","type":"User","site_admin":false},"body":"You can remove this now.","created_at":"2015-09-17T00:02:41Z","updated_at":"2015-09-17T00:02:41Z","html_url":"https://github.com/letsencrypt/boulder/pull/454#discussion_r39699868","pull_request_url":"https://api.github.com/repos/letsencrypt/boulder/pulls/454","_links":{"self":{"href":"https://api.github.com/repos/letsencrypt/boulder/pulls/comments/39699868"},"html":{"href":"https://github.com/letsencrypt/boulder/pull/454#discussion_r39699868"},"pull_request":{"href":"https://api.github.com/repos/letsencrypt/boulder/pulls/454"}}},"pull_request":{"url":"https://api.github.com/repos/letsencrypt/boulder/pulls/454","id":39729870,"html_url":"https://github.com/letsencrypt/boulder/pull/454","diff_url":"https://github.com/letsencrypt/boulder/pull/454.diff","patch_url":"https://github.com/letsencrypt/boulder/pull/454.patch","issue_url":"https://api.github.com/repos/letsencrypt/boulder/issues/454","number":454,"state":"open","locked":false,"title":"Submit issued certificates to CT logs","user":{"login":"rolandshoemaker","id":5080674,"avatar_url":"https://avatars.githubusercontent.com/u/5080674?v=3","gravatar_id":"","url":"https://api.github.com/users/rolandshoemaker","html_url":"https://github.com/rolandshoemaker","followers_url":"https://api.github.com/users/rolandshoemaker/followers","following_url":"https://api.github.com/users/rolandshoemaker/following{/other_user}","gists_url":"https://api.github.com/users/rolandshoemaker/gists{/gist_id}","starred_url":"https://api.github.com/users/rolandshoemaker/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rolandshoemaker/subscriptions","organizations_url":"https://api.github.com/users/rolandshoemaker/orgs","repos_url":"https://api.github.com/users/rolandshoemaker/repos","events_url":"https://api.github.com/users/rolandshoemaker/events{/privacy}","received_events_url":"https://api.github.com/users/rolandshoemaker/received_events","type":"User","site_admin":false},"body":"Half of a solution for #95 and #214. This adds a new component, `Publisher`, to Boulder which submits certificate chains to various CT logs (if any are configured). The submission method is called in a goroutine from `ca.IssueCertificate` so as to not block issuance. If the submission fails due to a retry-able problem it will retry `Config.Publisher.CT.SubmissionRetries` times before failing and logging that it was unable to submit the certificate (with its serial, the URI of the log being submitted to, and the number of times the submission was retried).\r\n\r\nWhile this does parse the returned SCT it doesn't do anything with it beyond checking the signature is a correctly formatted SHA256 ECDSA sig (no validation of signatures now/yet). Once we are able to marshal SCT structures into OCSP responses (Lack of support in `golang.org/x/crypto/ocsp` is blocking this now) we will want to store the SCT somewhere in SQL, my first thought is the CertificateStatus table, so that the `OCSP-Updater` read them out.\r\n\r\nI haven't implemented any of the second half, but it might make sense to include some amount of the SQL structure in this PR so that we will have an easier way to find certificates that haven't been submitted to logs so we can re-attempt to submit them (rather than just searching the logs for `Unable to submit certificate to CT log ...` messages). For now perhaps just a SCTSignature column?","created_at":"2015-07-10T22:19:37Z","updated_at":"2015-09-17T00:02:41Z","closed_at":null,"merged_at":null,"merge_commit_sha":"2ea3ecadf642dd0206a192f9e7d936af2bd4dab8","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/letsencrypt/boulder/pulls/454/commits","review_comments_url":"https://api.github.com/repos/letsencrypt/boulder/pulls/454/comments","review_comment_url":"https://api.github.com/repos/letsencrypt/boulder/pulls/comments{/number}","comments_url":"https://api.github.com/repos/letsencrypt/boulder/issues/454/comments","statuses_url":"https://api.github.com/repos/letsencrypt/boulder/statuses/8fa508a376ce91d3b3da97e50d86451748180108","head":{"label":"letsencrypt:ct-submission","ref":"ct-submission","sha":"8fa508a376ce91d3b3da97e50d86451748180108","user":{"login":"letsencrypt","id":9289019,"avatar_url":"https://avatars.githubusercontent.com/u/9289019?v=3","gravatar_id":"","url":"https://api.github.com/users/letsencrypt","html_url":"https://github.com/letsencrypt","followers_url":"https://api.github.com/users/letsencrypt/followers","following_url":"https://api.github.com/users/letsencrypt/following{/other_user}","gists_url":"https://api.github.com/users/letsencrypt/gists{/gist_id}","starred_url":"https://api.github.com/users/letsencrypt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/letsencrypt/subscriptions","organizations_url":"https://api.github.com/users/letsencrypt/orgs","repos_url":"https://api.github.com/users/letsencrypt/repos","events_url":"https://api.github.com/users/letsencrypt/events{/privacy}","received_events_url":"https://api.github.com/users/letsencrypt/received_events","type":"Organization","site_admin":false},"repo":{"id":28283593,"name":"boulder","full_name":"letsencrypt/boulder","owner":{"login":"letsencrypt","id":9289019,"avatar_url":"https://avatars.githubusercontent.com/u/9289019?v=3","gravatar_id":"","url":"https://api.github.com/users/letsencrypt","html_url":"https://github.com/letsencrypt","followers_url":"https://api.github.com/users/letsencrypt/followers","following_url":"https://api.github.com/users/letsencrypt/following{/other_user}","gists_url":"https://api.github.com/users/letsencrypt/gists{/gist_id}","starred_url":"https://api.github.com/users/letsencrypt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/letsencrypt/subscriptions","organizations_url":"https://api.github.com/users/letsencrypt/orgs","repos_url":"https://api.github.com/users/letsencrypt/repos","events_url":"https://api.github.com/users/letsencrypt/events{/privacy}","received_events_url":"https://api.github.com/users/letsencrypt/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/letsencrypt/boulder","description":"An ACME-based CA, written in Go.","fork":false,"url":"https://api.github.com/repos/letsencrypt/boulder","forks_url":"https://api.github.com/repos/letsencrypt/boulder/forks","keys_url":"https://api.github.com/repos/letsencrypt/boulder/keys{/key_id}","collaborators_url":"https://api.github.com/repos/letsencrypt/boulder/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/letsencrypt/boulder/teams","hooks_url":"https://api.github.com/repos/letsencrypt/boulder/hooks","issue_events_url":"https://api.github.com/repos/letsencrypt/boulder/issues/events{/number}","events_url":"https://api.github.com/repos/letsencrypt/boulder/events","assignees_url":"https://api.github.com/repos/letsencrypt/boulder/assignees{/user}","branches_url":"https://api.github.com/repos/letsencrypt/boulder/branches{/branch}","tags_url":"https://api.github.com/repos/letsencrypt/boulder/tags","blobs_url":"https://api.github.com/repos/letsencrypt/boulder/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/letsencrypt/boulder/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/letsencrypt/boulder/git/refs{/sha}","trees_url":"https://api.github.com/repos/letsencrypt/boulder/git/trees{/sha}","statuses_url":"https://api.github.com/repos/letsencrypt/boulder/statuses/{sha}","languages_url":"https://api.github.com/repos/letsencrypt/boulder/languages","stargazers_url":"https://api.github.com/repos/letsencrypt/boulder/stargazers","contributors_url":"https://api.github.com/repos/letsencrypt/boulder/contributors","subscribers_url":"https://api.github.com/repos/letsencrypt/boulder/subscribers","subscription_url":"https://api.github.com/repos/letsencrypt/boulder/subscription","commits_url":"https://api.github.com/repos/letsencrypt/boulder/commits{/sha}","git_commits_url":"https://api.github.com/repos/letsencrypt/boulder/git/commits{/sha}","comments_url":"https://api.github.com/repos/letsencrypt/boulder/comments{/number}","issue_comment_url":"https://api.github.com/repos/letsencrypt/boulder/issues/comments{/number}","contents_url":"https://api.github.com/repos/letsencrypt/boulder/contents/{+path}","compare_url":"https://api.github.com/repos/letsencrypt/boulder/compare/{base}...{head}","merges_url":"https://api.github.com/repos/letsencrypt/boulder/merges","archive_url":"https://api.github.com/repos/letsencrypt/boulder/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/letsencrypt/boulder/downloads","issues_url":"https://api.github.com/repos/letsencrypt/boulder/issues{/number}","pulls_url":"https://api.github.com/repos/letsencrypt/boulder/pulls{/number}","milestones_url":"https://api.github.com/repos/letsencrypt/boulder/milestones{/number}","notifications_url":"https://api.github.com/repos/letsencrypt/boulder/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/letsencrypt/boulder/labels{/name}","releases_url":"https://api.github.com/repos/letsencrypt/boulder/releases{/id}","created_at":"2014-12-21T00:29:54Z","updated_at":"2015-09-16T22:11:43Z","pushed_at":"2015-09-16T23:54:08Z","git_url":"git://github.com/letsencrypt/boulder.git","ssh_url":"git@github.com:letsencrypt/boulder.git","clone_url":"https://github.com/letsencrypt/boulder.git","svn_url":"https://github.com/letsencrypt/boulder","homepage":null,"size":25306,"stargazers_count":685,"watchers_count":685,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":62,"mirror_url":null,"open_issues_count":135,"forks":62,"open_issues":135,"watchers":685,"default_branch":"master"}},"base":{"label":"letsencrypt:master","ref":"master","sha":"dc4e848784f3945d93b6b43830b1777e19160b94","user":{"login":"letsencrypt","id":9289019,"avatar_url":"https://avatars.githubusercontent.com/u/9289019?v=3","gravatar_id":"","url":"https://api.github.com/users/letsencrypt","html_url":"https://github.com/letsencrypt","followers_url":"https://api.github.com/users/letsencrypt/followers","following_url":"https://api.github.com/users/letsencrypt/following{/other_user}","gists_url":"https://api.github.com/users/letsencrypt/gists{/gist_id}","starred_url":"https://api.github.com/users/letsencrypt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/letsencrypt/subscriptions","organizations_url":"https://api.github.com/users/letsencrypt/orgs","repos_url":"https://api.github.com/users/letsencrypt/repos","events_url":"https://api.github.com/users/letsencrypt/events{/privacy}","received_events_url":"https://api.github.com/users/letsencrypt/received_events","type":"Organization","site_admin":false},"repo":{"id":28283593,"name":"boulder","full_name":"letsencrypt/boulder","owner":{"login":"letsencrypt","id":9289019,"avatar_url":"https://avatars.githubusercontent.com/u/9289019?v=3","gravatar_id":"","url":"https://api.github.com/users/letsencrypt","html_url":"https://github.com/letsencrypt","followers_url":"https://api.github.com/users/letsencrypt/followers","following_url":"https://api.github.com/users/letsencrypt/following{/other_user}","gists_url":"https://api.github.com/users/letsencrypt/gists{/gist_id}","starred_url":"https://api.github.com/users/letsencrypt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/letsencrypt/subscriptions","organizations_url":"https://api.github.com/users/letsencrypt/orgs","repos_url":"https://api.github.com/users/letsencrypt/repos","events_url":"https://api.github.com/users/letsencrypt/events{/privacy}","received_events_url":"https://api.github.com/users/letsencrypt/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/letsencrypt/boulder","description":"An ACME-based CA, written in Go.","fork":false,"url":"https://api.github.com/repos/letsencrypt/boulder","forks_url":"https://api.github.com/repos/letsencrypt/boulder/forks","keys_url":"https://api.github.com/repos/letsencrypt/boulder/keys{/key_id}","collaborators_url":"https://api.github.com/repos/letsencrypt/boulder/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/letsencrypt/boulder/teams","hooks_url":"https://api.github.com/repos/letsencrypt/boulder/hooks","issue_events_url":"https://api.github.com/repos/letsencrypt/boulder/issues/events{/number}","events_url":"https://api.github.com/repos/letsencrypt/boulder/events","assignees_url":"https://api.github.com/repos/letsencrypt/boulder/assignees{/user}","branches_url":"https://api.github.com/repos/letsencrypt/boulder/branches{/branch}","tags_url":"https://api.github.com/repos/letsencrypt/boulder/tags","blobs_url":"https://api.github.com/repos/letsencrypt/boulder/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/letsencrypt/boulder/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/letsencrypt/boulder/git/refs{/sha}","trees_url":"https://api.github.com/repos/letsencrypt/boulder/git/trees{/sha}","statuses_url":"https://api.github.com/repos/letsencrypt/boulder/statuses/{sha}","languages_url":"https://api.github.com/repos/letsencrypt/boulder/languages","stargazers_url":"https://api.github.com/repos/letsencrypt/boulder/stargazers","contributors_url":"https://api.github.com/repos/letsencrypt/boulder/contributors","subscribers_url":"https://api.github.com/repos/letsencrypt/boulder/subscribers","subscription_url":"https://api.github.com/repos/letsencrypt/boulder/subscription","commits_url":"https://api.github.com/repos/letsencrypt/boulder/commits{/sha}","git_commits_url":"https://api.github.com/repos/letsencrypt/boulder/git/commits{/sha}","comments_url":"https://api.github.com/repos/letsencrypt/boulder/comments{/number}","issue_comment_url":"https://api.github.com/repos/letsencrypt/boulder/issues/comments{/number}","contents_url":"https://api.github.com/repos/letsencrypt/boulder/contents/{+path}","compare_url":"https://api.github.com/repos/letsencrypt/boulder/compare/{base}...{head}","merges_url":"https://api.github.com/repos/letsencrypt/boulder/merges","archive_url":"https://api.github.com/repos/letsencrypt/boulder/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/letsencrypt/boulder/downloads","issues_url":"https://api.github.com/repos/letsencrypt/boulder/issues{/number}","pulls_url":"https://api.github.com/repos/letsencrypt/boulder/pulls{/number}","milestones_url":"https://api.github.com/repos/letsencrypt/boulder/milestones{/number}","notifications_url":"https://api.github.com/repos/letsencrypt/boulder/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/letsencrypt/boulder/labels{/name}","releases_url":"https://api.github.com/repos/letsencrypt/boulder/releases{/id}","created_at":"2014-12-21T00:29:54Z","updated_at":"2015-09-16T22:11:43Z","pushed_at":"2015-09-16T23:54:08Z","git_url":"git://github.com/letsencrypt/boulder.git","ssh_url":"git@github.com:letsencrypt/boulder.git","clone_url":"https://github.com/letsencrypt/boulder.git","svn_url":"https://github.com/letsencrypt/boulder","homepage":null,"size":25306,"stargazers_count":685,"watchers_count":685,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":62,"mirror_url":null,"open_issues_count":135,"forks":62,"open_issues":135,"watchers":685,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/letsencrypt/boulder/pulls/454"},"html":{"href":"https://github.com/letsencrypt/boulder/pull/454"},"issue":{"href":"https://api.github.com/repos/letsencrypt/boulder/issues/454"},"comments":{"href":"https://api.github.com/repos/letsencrypt/boulder/issues/454/comments"},"review_comments":{"href":"https://api.github.com/repos/letsencrypt/boulder/pulls/454/comments"},"review_comment":{"href":"https://api.github.com/repos/letsencrypt/boulder/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/letsencrypt/boulder/pulls/454/commits"},"statuses":{"href":"https://api.github.com/repos/letsencrypt/boulder/statuses/8fa508a376ce91d3b3da97e50d86451748180108"}}}}