├── .gitignore ├── LICENSE ├── README.markdown └── ct-submit.go /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | *~ 3 | /ct-submit 4 | !.git* 5 | !.mailmap 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016 Graham Edgecombe 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ct-submit 2 | ========= 3 | 4 | Introduction 5 | ------------ 6 | 7 | `ct-submit` is a program that submits X.509 certificate chains to 8 | [Certificate Transparency][ct] log servers. It returns the Signed Certificate 9 | Timestamp structure in a format suitable for use with Apache's 10 | [mod\_ssl\_ct][apache] module and [nginx-ct][nginx]. 11 | 12 | Building 13 | -------- 14 | 15 | `ct-submit` is written in [Go][go]. Just run `go build` to build it. 16 | 17 | Usage 18 | ----- 19 | 20 | `ct-submit` takes a single argument - the URL of the log server. If the scheme 21 | is not specified it defaults to `https://`. It reads the certificate chain in 22 | PEM format from `stdin`. The leaf certificate should be the first certificate 23 | in the chain, followed by any intermediate certificates and, optionally, the 24 | root certificate. 25 | 26 | The encoded SCT structure is written in binary to `stdout`. 27 | 28 | The following example demonstrates submitting the chain in `gpe.pem` to 29 | Google's pilot log server. The SCT is written to `gpe.sct`, which is in a format 30 | suitable for use with Apache's mod\_ssl\_ct module and nginx-ct. 31 | 32 | $ ./ct-submit ct.googleapis.com/pilot gpe.sct 33 | $ xxd gpe.sct 34 | 00000000: 00a4 b909 90b4 1858 1487 bb13 a2cc 6770 .......X......gp 35 | 00000010: 0a3c 3598 04f9 1bdf b8e3 77cd 0ec8 0ddc .<5.......w..... 36 | 00000020: 1000 0001 4bc7 e617 c800 0004 0300 4830 ....K.........H0 37 | 00000030: 4602 2100 b9fe e206 f0f5 f600 93d5 e04c F.!............L 38 | 00000040: d2fd 75c9 e1fc a5c8 4812 a8b7 bc2c eb0c ..u.....H....,.. 39 | 00000050: ee16 1fe9 0221 008a 5974 e1b6 a0e0 281a .....!..Yt....(. 40 | 00000060: 61e8 3447 895f 7ad4 2f70 f528 6133 a445 a.4G._z./p.(a3.E 41 | 00000070: 4fd4 ab60 ba36 db O..`.6. 42 | $ 43 | 44 | License 45 | ------- 46 | 47 | `ct-submit` is available under the terms of the ISC license, which is similar to 48 | the 2-clause BSD license. See the `LICENSE` file for the copyright information 49 | and licensing terms. 50 | 51 | [ct]: http://www.certificate-transparency.org/ 52 | [apache]: https://httpd.apache.org/docs/trunk/mod/mod_ssl_ct.html 53 | [nginx]: https://github.com/grahamedgecombe/nginx-ct 54 | [go]: https://golang.org/ 55 | -------------------------------------------------------------------------------- /ct-submit.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2016 Graham Edgecombe 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted, provided that the above 5 | // copyright notice and this permission notice appear in all copies. 6 | // 7 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | package main 16 | 17 | import ( 18 | "bytes" 19 | _ "crypto/sha512" 20 | "encoding/base64" 21 | "encoding/binary" 22 | "encoding/json" 23 | "encoding/pem" 24 | "errors" 25 | "fmt" 26 | "io" 27 | "io/ioutil" 28 | "net/http" 29 | "net/url" 30 | "os" 31 | "strings" 32 | ) 33 | 34 | type addChain struct { 35 | Chain []string `json:"chain"` 36 | } 37 | 38 | type signedCertificateTimestamp struct { 39 | Version uint8 `json:"sct_version"` 40 | LogID string `json:"id"` 41 | Timestamp int64 `json:"timestamp"` 42 | Extensions string `json:"extensions"` 43 | Signature string `json:"signature"` 44 | } 45 | 46 | func (sct signedCertificateTimestamp) Write(w io.Writer) error { 47 | // Version 48 | if err := binary.Write(w, binary.BigEndian, sct.Version); err != nil { 49 | return err 50 | } 51 | 52 | // LogID 53 | bytes, err := base64.StdEncoding.DecodeString(sct.LogID) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | _, err = w.Write(bytes) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | // Timestamp 64 | if err := binary.Write(w, binary.BigEndian, sct.Timestamp); err != nil { 65 | return err 66 | } 67 | 68 | // Extensions 69 | bytes, err = base64.StdEncoding.DecodeString(sct.Extensions) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | length := len(bytes) 75 | if length > 65535 { 76 | return errors.New("extensions are too long") 77 | } 78 | 79 | if err := binary.Write(w, binary.BigEndian, uint16(length)); err != nil { 80 | return err 81 | } 82 | 83 | _, err = w.Write(bytes) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | // Signature 89 | bytes, err = base64.StdEncoding.DecodeString(sct.Signature) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | _, err = w.Write(bytes) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | return nil 100 | } 101 | 102 | func main() { 103 | // parse args 104 | if len(os.Args) != 2 { 105 | fmt.Fprintf(os.Stderr, `usage: ct-submit 106 | 107 | ct-submit reads a PEM-encoded X.509 certificate chain from stdin and submits it 108 | to the given Certificate Transparency log server. The Signed Certificate 109 | Timestamp structure returned by the log server is written to stdout in binary. 110 | 111 | The leaf certificate should be the first certificate in the chain, followed by 112 | any intermediate certificates and, optionally, the root certificate. 113 | 114 | The signature of the SCT is not verified. 115 | `) 116 | os.Exit(1) 117 | } 118 | 119 | logServer := os.Args[1] 120 | 121 | // read certificate chain from stdin 122 | in, err := ioutil.ReadAll(os.Stdin) 123 | if err != nil { 124 | panic(err) 125 | } 126 | 127 | msg := addChain{} 128 | for { 129 | block, remaining := pem.Decode(in) 130 | in = remaining 131 | 132 | if block == nil { 133 | break 134 | } 135 | 136 | if block.Type != "CERTIFICATE" { 137 | continue 138 | } 139 | 140 | msg.Chain = append(msg.Chain, base64.StdEncoding.EncodeToString(block.Bytes)) 141 | } 142 | 143 | // construct add-chain message 144 | payload, err := json.Marshal(msg) 145 | if err != nil { 146 | panic(err) 147 | } 148 | 149 | // construct add-chain URL 150 | if !strings.Contains(logServer, "://") { 151 | logServer = "https://" + logServer 152 | } 153 | 154 | if !strings.HasSuffix(logServer, "/") { 155 | logServer = logServer + "/" 156 | } 157 | 158 | addChainURL, err := url.Parse(logServer) 159 | if err != nil { 160 | panic(err) 161 | } 162 | 163 | addChainURL, err = addChainURL.Parse("ct/v1/add-chain") 164 | if err != nil { 165 | panic(err) 166 | } 167 | 168 | // send add-chain message to the log 169 | response, err := http.Post(addChainURL.String(), "application/json", bytes.NewReader(payload)) 170 | if err != nil { 171 | panic(err) 172 | } 173 | 174 | if response.StatusCode != http.StatusOK { 175 | fmt.Fprintf(os.Stderr, "unexpected status %s from log server:\n\n", response.Status) 176 | io.Copy(os.Stderr, response.Body) 177 | os.Exit(1) 178 | } 179 | 180 | // decode JSON SCT structure 181 | payload, err = ioutil.ReadAll(response.Body) 182 | if err != nil { 183 | panic(err) 184 | } 185 | 186 | sct := signedCertificateTimestamp{} 187 | if err = json.Unmarshal(payload, &sct); err != nil { 188 | panic(err) 189 | } 190 | 191 | // write binary SCT structure to stdout 192 | if err = sct.Write(os.Stdout); err != nil { 193 | panic(err) 194 | } 195 | } 196 | --------------------------------------------------------------------------------