├── .gitignore ├── README.md ├── acme.go ├── jws.go ├── main.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | *.key 3 | *.pem 4 | *.crt 5 | *.csr 6 | Makefile -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-acme 2 | 3 | Automated Certificate Management Environment (ACME) client written in Go using 4 | just standard library. No external dependencies required. 5 | 6 | ## Features 7 | 8 | - Authorizing domain names in parallel to greatly speed up the 9 | issuance of multi-domain SAN certificates. 10 | - Generate certificates from another host holding your account key. There is no 11 | need to keep the account key on the public facing server. 12 | - Single binary for easy deployment. Just drop and run. 13 | 14 | 15 | ## Usage 16 | 17 | The workflow of `go-acme` is very simple: 18 | 19 | 1. Setup Nginx to reverse proxy ACME HTTP challenges. 20 | 2. Launch `go-acme` to authorize domain names and generate certificates. 21 | 3. Done. 22 | 23 | 24 | Add the following section to `server` section of your nginx config. It will 25 | forward requests for ACME HTTP challenges to a server listening on port 81. You 26 | can use any port number, but it is recommended that you use a privileged port so 27 | that only root can bind to for security reasons. 28 | 29 | ```nginx 30 | location ^~ /.well-known/acme-challenge/ { 31 | proxy_pass http://127.0.0.1:81; 32 | } 33 | ``` 34 | 35 | Then restart Nginx 36 | 37 | ```sh 38 | nginx -s reload 39 | ``` 40 | 41 | Generate a 4096-bit RSA account key if you do not have one yet 42 | 43 | ```sh 44 | acme -genrsa 4096 > account.key 45 | ``` 46 | 47 | Generate a 2048-bit RSA certificate key if you do not have one yet 48 | 49 | ```sh 50 | acme -genrsa 2048 > cert.key 51 | ``` 52 | 53 | To run `go-acme` on the same host with the server, execute 54 | 55 | ```sh 56 | acme -addr 127.0.0.1:81 -acckey account.key -crtkey cert.key -domains example.com,www.example.com > cert.pem 57 | ``` 58 | 59 | and wait for your domain certifcate and issuer certificate to be put into `cert.pem` file. 60 | 61 | 62 | Alternatively, you can run `go-acme` on another host. This has the benefit that 63 | there is no need to put the private account key on the public-facing web server. 64 | 65 | To do so, you need to use SSH to forward port 81 on the server to a free port 66 | (8181 in the example below) on the host running `go-acme`. 67 | 68 | ```sh 69 | ssh -N -T -R 81:127.0.0.1:8181 server-hostname 70 | ``` 71 | 72 | and then run `go-acme` listening on the forwarded port (8181) 73 | 74 | ```sh 75 | acme -addr 127.0.0.1:8181 -acckey account.key -crtkey cert.key -domains example.com,www.example.com > cert.pem 76 | ``` 77 | 78 | After that you need to copy `cert.key` and `cert.pem` files back to the web server. 79 | 80 | 81 | 82 | 83 | ## TODO 84 | 85 | - Certificate revokation 86 | -------------------------------------------------------------------------------- /acme.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "crypto/rsa" 7 | "crypto/x509" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "net/http" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | // ACME Challenge object 20 | type Challenge struct { 21 | Type string 22 | Status string 23 | URI string 24 | Token string 25 | Error *struct { 26 | Type string 27 | Detail string 28 | } 29 | } 30 | 31 | // ACME Authorization object 32 | type Auth struct { 33 | Challenges []Challenge 34 | Expires time.Time 35 | Status string 36 | Identifier struct { 37 | Type string // ACME spec only supports Type=DNS 38 | Value string // so Value must be a domain name 39 | } 40 | Combinations [][]int // pointers to sets of challenges to solve 41 | } 42 | 43 | // ACME API 44 | type ACME struct { 45 | URL string // ACME directory URL 46 | Dir struct { 47 | NewReg string `json:"new-reg"` 48 | NewCert string `json:"new-cert"` 49 | NewAuthz string `json:"new-authz"` 50 | RevokeCert string `json:"revoke-cert"` 51 | } 52 | key *rsa.PrivateKey 53 | keyAuth map[string]string // token => key authorization 54 | rwmutex sync.RWMutex 55 | noncePool chan string 56 | } 57 | 58 | // Connect to the ACME server at the url using the given account key 59 | func OpenACME(url string, key *rsa.PrivateKey) (*ACME, error) { 60 | acme := &ACME{ 61 | URL: url, 62 | key: key, 63 | keyAuth: make(map[string]string), 64 | noncePool: make(chan string, 100), 65 | } 66 | rsp, err := http.Get(url) 67 | if err != nil { 68 | return nil, err 69 | } 70 | if rsp.StatusCode != 200 { 71 | return nil, fmt.Errorf("ACME server error: %s", rsp.Status) 72 | } 73 | acme.noncePool <- rsp.Header.Get("Replay-Nonce") 74 | if err := json.NewDecoder(rsp.Body).Decode(&acme.Dir); err != nil { 75 | return nil, err 76 | } 77 | return acme, nil 78 | } 79 | 80 | // Post a JWS signed payload to the given url 81 | func (a *ACME) do(url string, payload []byte) (*http.Response, error) { 82 | var nonce string 83 | select { 84 | case nonce = <-a.noncePool: 85 | default: // nonce pool is empty 86 | rsp, err := http.Head(a.URL) 87 | if err != nil { 88 | return nil, err 89 | } 90 | nonce = rsp.Header.Get("Replay-Nonce") 91 | } 92 | 93 | rsp, err := http.Post(url, "", bytes.NewBuffer(jsonWebSign(a.key, payload, nonce))) 94 | if err != nil { 95 | return nil, err 96 | } 97 | go func() { // put back a new nonce 98 | a.noncePool <- rsp.Header.Get("Replay-Nonce") 99 | }() 100 | return rsp, nil 101 | } 102 | 103 | // Update the registration object at regURL with the agreement to Terms of 104 | // Service at tosURL. 105 | func (a *ACME) agreeTOS(regURL, tosURL string) error { 106 | payload, err := json.Marshal(map[string]string{ 107 | "resource": "reg", 108 | "agreement": tosURL, 109 | }) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | rsp, err := a.do(regURL, payload) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | if rsp.StatusCode != 202 { 120 | return fmt.Errorf("failed to agree terms of service: HTTP %s", rsp.Status) 121 | } 122 | return nil 123 | } 124 | 125 | // Register the account key, even if the key is already registred. 126 | func (a *ACME) NewReg() error { 127 | payload, err := json.Marshal(map[string]string{ 128 | "resource": "new-reg", 129 | }) 130 | if err != nil { 131 | return err 132 | } 133 | rsp, err := a.do(a.Dir.NewReg, payload) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | switch rsp.StatusCode { 139 | case 201: // new registration ok 140 | links := parseLinks(rsp.Header["Link"]) 141 | // agree to Terms of Service if present 142 | if tos, ok := links["terms-of-service"]; ok { 143 | a.agreeTOS(rsp.Header.Get("Location"), tos) 144 | } 145 | case 409: // key already registered 146 | default: // error 147 | return fmt.Errorf("key registration failed: HTTP %s", rsp.Status) 148 | } 149 | return nil 150 | } 151 | 152 | // Authorize a domain name. Currently only http-01 method is supported. 153 | func (a *ACME) NewAuthz(domain string) error { 154 | payload, err := json.Marshal(map[string]interface{}{ 155 | "resource": "new-authz", 156 | "identifier": map[string]string{ 157 | "type": "dns", 158 | "value": domain, 159 | }, 160 | }) 161 | if err != nil { 162 | return err 163 | } 164 | 165 | rsp, err := a.do(a.Dir.NewAuthz, payload) 166 | if err != nil { 167 | return err 168 | } 169 | if rsp.StatusCode != 201 { 170 | return fmt.Errorf("failed to create authorization: HTTP %s", rsp.Status) 171 | } 172 | 173 | auth := new(Auth) 174 | if err := json.NewDecoder(rsp.Body).Decode(auth); err != nil { 175 | return err 176 | } 177 | 178 | for _, c := range auth.Challenges { 179 | switch c.Type { 180 | case "http-01": 181 | if err := a.http01(domain, c.URI, c.Token); err != nil { 182 | return err 183 | } 184 | } 185 | } 186 | 187 | return nil 188 | } 189 | 190 | // Solve http01 challenge at at uri fro domain using token 191 | func (a *ACME) http01(domain, uri, token string) error { 192 | keyAuth := token + "." + b64(rsa2jwk(a.key).Thumbprint(crypto.SHA256)) 193 | 194 | a.rwmutex.Lock() 195 | a.keyAuth[token] = keyAuth 196 | a.rwmutex.Unlock() 197 | 198 | // verify that the key auth for the token can be fetched 199 | url := "http://" + domain + ACMEChallengePathPrefix + token 200 | rsp, err := http.Get(url) 201 | if err != nil { 202 | return err 203 | } 204 | if rsp.StatusCode != 200 { 205 | return fmt.Errorf("failed to fetch key authorization at %s", url) 206 | } 207 | b, err := ioutil.ReadAll(rsp.Body) 208 | if err != nil { 209 | return err 210 | } 211 | if bytes.Equal(b, []byte(keyAuth)) != true { 212 | return fmt.Errorf("incorrect key authorization at %s", url) 213 | } 214 | 215 | // ask the ACME server to start challenge validation 216 | payload, err := json.Marshal(map[string]string{ 217 | "resource": "challenge", 218 | "keyAuthorization": keyAuth, 219 | }) 220 | if err != nil { 221 | return err 222 | } 223 | rsp, err = a.do(uri, payload) 224 | if err != nil { 225 | return err 226 | } 227 | if rsp.StatusCode != 202 { 228 | return fmt.Errorf("failed to post challenge") 229 | } 230 | 231 | c := new(Challenge) 232 | if err := json.NewDecoder(rsp.Body).Decode(c); err != nil { 233 | return err 234 | } 235 | 236 | // poll to get latest validation status 237 | for { 238 | time.Sleep(2 * time.Second) 239 | rsp, err := http.Get(c.URI) 240 | if err != nil { 241 | return err 242 | } 243 | if rsp.StatusCode != 202 { 244 | return fmt.Errorf("HTTP %s", rsp.Status) 245 | } 246 | c := new(Challenge) 247 | if err := json.NewDecoder(rsp.Body).Decode(c); err != nil { 248 | return err 249 | } 250 | switch c.Status { 251 | case "", "pending", "processing": 252 | // missing status defaults to pending. wait and poll again. 253 | case "valid": 254 | return nil 255 | default: 256 | if c.Error != nil { 257 | return fmt.Errorf("%s [%s] %s", c.Status, c.Error.Type, c.Error.Detail) 258 | } 259 | return fmt.Errorf("%s", c.Status) 260 | } 261 | } 262 | } 263 | 264 | // HTTP handler to solve ACME HTTP challenge 265 | func (a *ACME) ServeHTTP(w http.ResponseWriter, r *http.Request) { 266 | tok := strings.TrimPrefix(r.URL.Path, ACMEChallengePathPrefix) 267 | a.rwmutex.RLock() 268 | keyAuth, ok := a.keyAuth[tok] 269 | a.rwmutex.RUnlock() 270 | if ok { 271 | io.WriteString(w, keyAuth) 272 | } else { 273 | http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) 274 | } 275 | } 276 | 277 | // Create a new certificate using the given certificate signing request 278 | func (a *ACME) NewCert(csr []byte) (domainCrt *x509.Certificate, issuerCrt *x509.Certificate, err error) { 279 | payload, err := json.Marshal(map[string]string{ 280 | "resource": "new-cert", 281 | "csr": b64(csr), 282 | }) 283 | if err != nil { 284 | return nil, nil, err 285 | } 286 | 287 | rsp, err := a.do(a.Dir.NewCert, payload) 288 | if err != nil { 289 | return nil, nil, err 290 | } 291 | 292 | if rsp.StatusCode != 201 { 293 | return nil, nil, fmt.Errorf("failed to get domain certificate: HTTP %s", rsp.Status) 294 | } 295 | 296 | // The server might not return the certificate in the body, in which case 297 | // we need to poll the Location header to get the actual certificate. If 298 | // the certificate is unavailable, GET request to the Location header will 299 | // return 202 Accepted with a Retry-After header. 300 | for rsp.Header.Get("Content-Type") != "application/pkix-cert" { 301 | delay, _ := strconv.Atoi(rsp.Header.Get("Retry-After")) 302 | if delay == 0 { 303 | delay = 2 304 | } 305 | time.Sleep(time.Duration(delay) * time.Second) 306 | rsp, err = http.Get(rsp.Header.Get("Location")) 307 | if err != nil { 308 | return nil, nil, err 309 | } 310 | } 311 | 312 | // now the certificate should be available 313 | domainCrt, err = parseCert(rsp.Body) 314 | if err != nil { 315 | return nil, nil, err 316 | } 317 | 318 | // fetch issuer certificate 319 | links := parseLinks(rsp.Header["Link"]) 320 | issuer, ok := links["up"] 321 | if !ok { 322 | return nil, nil, fmt.Errorf("issuer certificate not specified") 323 | } 324 | rsp, err = http.Get(issuer) 325 | if err != nil { 326 | return nil, nil, err 327 | } 328 | if rsp.StatusCode != 200 { 329 | return nil, nil, fmt.Errorf("failed to fetch issuer certificate: HTTP %s", rsp.Status) 330 | } 331 | issuerCrt, err = parseCert(rsp.Body) 332 | if err != nil { 333 | return nil, nil, err 334 | } 335 | return domainCrt, issuerCrt, nil 336 | } 337 | -------------------------------------------------------------------------------- /jws.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "encoding/json" 8 | "math/big" 9 | ) 10 | 11 | // JSON Web Key public RSA key 12 | type JSONWebKey struct { 13 | Type string 14 | E int 15 | N *big.Int 16 | } 17 | 18 | // Make a JSON Web Key from an RSA private key 19 | func rsa2jwk(key *rsa.PrivateKey) *JSONWebKey { 20 | pub := key.Public().(*rsa.PublicKey) 21 | return &JSONWebKey{Type: "RSA", E: pub.E, N: pub.N} 22 | } 23 | 24 | func (k *JSONWebKey) MarshalJSON() ([]byte, error) { 25 | d := map[string]string{ 26 | "kty": k.Type, 27 | "e": b64(big.NewInt(int64(k.E)).Bytes()), 28 | "n": b64(k.N.Bytes()), 29 | } 30 | return json.Marshal(d) 31 | } 32 | 33 | // Compute the thumbprint of the JWK using the given crypto hash 34 | func (k *JSONWebKey) Thumbprint(hash crypto.Hash) []byte { 35 | // NOTE: This is not _strictly_ correct per RFC7638, as it requires that 36 | // JWK JSON object to have lexicographically ordered keys before computing 37 | // the thumbprint. Additionally, all unnecessary whitespaces must be trimmed. 38 | // Go's default JSON serialization of map fits the requirement for this case. 39 | 40 | j, err := json.Marshal(k) 41 | if err != nil { 42 | panic(err) // this should never happen 43 | } 44 | h := hash.New() 45 | h.Write(j) 46 | return h.Sum(nil) 47 | } 48 | 49 | // RFC7518 JSON Web Algorithms RS256 algorithm: RSA with SHA256 50 | func rs256(key *rsa.PrivateKey, data []byte) []byte { 51 | h := crypto.SHA256.New() 52 | h.Write(data) 53 | sig, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, h.Sum(nil)) 54 | if err != nil { 55 | panic(err) // this should never happen 56 | } 57 | return sig 58 | } 59 | 60 | // RFC7515 JSON Web Signature JSON serialization with replay protection 61 | func jsonWebSign(key *rsa.PrivateKey, payload []byte, nonce string) []byte { 62 | jwk := rsa2jwk(key) 63 | 64 | // JWS protected header 65 | protected := map[string]interface{}{ 66 | "alg": "RS256", 67 | "jwk": jwk, 68 | "nonce": nonce, 69 | } 70 | b, err := json.Marshal(protected) 71 | if err != nil { 72 | panic(err) // this should never happen 73 | } 74 | 75 | protected64 := b64(b) 76 | payload64 := b64(payload) 77 | 78 | // JWS JSON serializaton 79 | j, err := json.Marshal(map[string]string{ 80 | "protected": protected64, 81 | "payload": payload64, 82 | "signature": b64(rs256(key, []byte(protected64+"."+payload64))), 83 | }) 84 | if err != nil { 85 | panic(err) // this should never happen 86 | } 87 | return j 88 | } 89 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "flag" 8 | "log" 9 | "net/http" 10 | "os" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | const ( 16 | ACMEChallengePathPrefix = "/.well-known/acme-challenge/" 17 | LetsEncryptStaging = "https://acme-staging.api.letsencrypt.org/directory" 18 | LetsEncryptProduction = "https://acme-v01.api.letsencrypt.org/directory" 19 | ) 20 | 21 | var cfg struct { 22 | accKey string // path to account key 23 | crtKey string // path to certificate key 24 | Addr string 25 | Domains string // comma-separated domains 26 | API string 27 | GenRSA int 28 | Delay time.Duration 29 | } 30 | 31 | func init() { 32 | log.SetFlags(0) // do not log date 33 | flag.StringVar(&cfg.accKey, "acckey", "", "path to account key") 34 | flag.StringVar(&cfg.crtKey, "crtkey", "", "path to certificate key") 35 | flag.StringVar(&cfg.Addr, "addr", "127.0.0.1:81", "challenge server address") 36 | flag.StringVar(&cfg.Domains, "domains", "", "comma-separated list of up to 100 domain names") 37 | flag.StringVar(&cfg.API, "api", LetsEncryptProduction, "ACME API URL") 38 | flag.IntVar(&cfg.GenRSA, "genrsa", 0, "generate RSA private key of the given bits in length") 39 | flag.DurationVar(&cfg.Delay, "delay", 100*time.Millisecond, "delay per authorization to avoid hitting rate limit") 40 | flag.Parse() 41 | } 42 | 43 | func main() { 44 | 45 | if cfg.GenRSA > 0 { 46 | if err := genKey(os.Stdout, cfg.GenRSA); err != nil { 47 | log.Printf("Failed to generate RSA key: %v", err) 48 | } 49 | return 50 | } 51 | 52 | domains := strings.Split(cfg.Domains, ",") 53 | if len(domains) > 100 { 54 | log.Fatalf("Too many domains (%d > 100)", len(domains)) 55 | } 56 | 57 | // read account key 58 | accKey, err := openKey(cfg.accKey) 59 | if err != nil { 60 | log.Fatalf("Failed to parse account key: %s", err) 61 | } 62 | 63 | // read certificate key 64 | crtKey, err := openKey(cfg.crtKey) 65 | if err != nil { 66 | log.Fatalf("Failed to parse certificate key: %s", err) 67 | } 68 | 69 | log.Printf("Connecting to ACME server at %s", cfg.API) 70 | acme, err := OpenACME(cfg.API, accKey) 71 | if err != nil { 72 | log.Fatalf("Failed to connect to ACME server: %s", err) 73 | } 74 | 75 | // start the challenge server in background 76 | log.Printf("Responding to ACME challenges at http://%s", cfg.Addr) 77 | go http.ListenAndServe(cfg.Addr, acme) 78 | 79 | log.Printf("Registering account key") 80 | if err := acme.NewReg(); err != nil { 81 | log.Fatalf("Failed to register account key: %s", err) 82 | } 83 | 84 | // authorize domains in parallel 85 | type Done struct { 86 | Domain string 87 | Error error 88 | } 89 | ch := make(chan Done) 90 | for _, domain := range domains { 91 | go func(domain string) { 92 | log.Printf("Authorizing domain %s", domain) 93 | done := Done{Domain: domain} 94 | if err := acme.NewAuthz(domain); err != nil { 95 | done.Error = err 96 | } 97 | ch <- done 98 | }(domain) 99 | time.Sleep(cfg.Delay) // sleep 0.1 sec to avoid hitting rate limit 100 | } 101 | 102 | // collect authorization result 103 | failed := false 104 | for range domains { 105 | if done := <-ch; done.Error != nil { 106 | failed = true 107 | log.Printf("Failed to authorize domain %s: %s", done.Domain, done.Error) 108 | } else { 109 | log.Printf("Authorized domain %s", done.Domain) 110 | } 111 | } 112 | if failed { 113 | log.Fatalln("Some domains failed authorization") 114 | } 115 | 116 | // create certificate signing request 117 | tpl := &x509.CertificateRequest{DNSNames: domains} 118 | csr, err := x509.CreateCertificateRequest(rand.Reader, tpl, crtKey) 119 | if err != nil { 120 | log.Fatalf("Failed to create certificate request: %s", err) 121 | } 122 | 123 | log.Printf("Fetching certificates") 124 | domainCrt, issuerCrt, err := acme.NewCert(csr) 125 | if err != nil { 126 | log.Fatalf("Failed to fetch certificates: %s", err) 127 | } 128 | 129 | // print domain certificate in PEM to stdout 130 | if err := pem.Encode(os.Stdout, &pem.Block{ 131 | Type: "CERTIFICATE", 132 | Bytes: domainCrt.Raw, 133 | }); err != nil { 134 | log.Fatalln(err) 135 | } 136 | 137 | // print issuer certificate in PEM to stdout 138 | if err := pem.Encode(os.Stdout, &pem.Block{ 139 | Type: "CERTIFICATE", 140 | Bytes: issuerCrt.Raw, 141 | }); err != nil { 142 | log.Fatalln(err) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/base64" 8 | "encoding/pem" 9 | "errors" 10 | "io" 11 | "io/ioutil" 12 | "os" 13 | "regexp" 14 | ) 15 | 16 | // base64url encoding without padding 17 | func b64(s []byte) string { return base64.RawURLEncoding.EncodeToString(s) } 18 | 19 | // Parse HTTP Link header to get rel => link map 20 | func parseLinks(links []string) map[string]string { 21 | re := regexp.MustCompile(`<(.*)>;\s*rel="(.*)"`) 22 | m := make(map[string]string) 23 | for _, link := range links { 24 | rs := re.FindStringSubmatch(link) 25 | m[rs[2]] = rs[1] 26 | } 27 | return m 28 | } 29 | 30 | // Parse PEM-encoded RSA private key from r. 31 | func parseKey(r io.Reader) (*rsa.PrivateKey, error) { 32 | b, err := ioutil.ReadAll(r) 33 | if err != nil { 34 | return nil, err 35 | } 36 | blk := new(pem.Block) 37 | // find the first RSA private key 38 | for { 39 | blk, b = pem.Decode(b) 40 | if blk != nil && blk.Type == "RSA PRIVATE KEY" { 41 | return x509.ParsePKCS1PrivateKey(blk.Bytes) 42 | } 43 | } 44 | return nil, errors.New("no RSA private key found") 45 | } 46 | 47 | // Open PEM-encoded RSA private key at the given path. 48 | func openKey(path string) (*rsa.PrivateKey, error) { 49 | r, err := os.Open(path) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return parseKey(r) 55 | } 56 | 57 | // Parse PEM-encoded x509 certificate 58 | func parseCert(r io.Reader) (*x509.Certificate, error) { 59 | der, err := ioutil.ReadAll(r) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | return x509.ParseCertificate(der) 65 | } 66 | 67 | // Generate RSA private key and write to w in PEM format. 68 | func genKey(w io.Writer, bits int) error { 69 | key, err := rsa.GenerateKey(rand.Reader, bits) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | return pem.Encode(w, &pem.Block{ 75 | Type: "RSA PRIVATE KEY", 76 | Bytes: x509.MarshalPKCS1PrivateKey(key), 77 | }) 78 | } 79 | --------------------------------------------------------------------------------