2015-06-08 00:36:07 +00:00
package acme
import (
"crypto/rsa"
2015-06-13 01:55:53 +00:00
"encoding/base64"
2015-06-08 00:36:07 +00:00
"encoding/json"
"errors"
2015-06-08 21:54:15 +00:00
"fmt"
2015-06-08 00:36:07 +00:00
"io/ioutil"
"log"
"net/http"
"os"
"regexp"
"strings"
)
// Logger is used to log errors; if nil, the default log.Logger is used.
var Logger * log . Logger
// logger is an helper function to retrieve the available logger
func logger ( ) * log . Logger {
if Logger == nil {
Logger = log . New ( os . Stderr , "" , log . LstdFlags )
}
return Logger
}
// User interface is to be implemented by users of this library.
// It is used by the client type to get user specific information.
type User interface {
GetEmail ( ) string
GetRegistration ( ) * RegistrationResource
GetPrivateKey ( ) * rsa . PrivateKey
}
2015-06-13 02:50:36 +00:00
// Interface for all challenge solvers to implement.
2015-06-11 13:31:09 +00:00
type solver interface {
2015-06-13 16:37:30 +00:00
CanSolve ( domain string ) bool
2015-06-13 01:55:53 +00:00
Solve ( challenge challenge , domain string ) error
2015-06-10 13:11:01 +00:00
}
2015-06-08 00:36:07 +00:00
// Client is the user-friendy way to ACME
type Client struct {
2015-09-26 20:59:16 +00:00
directory directory
user User
jws * jws
keyBits int
solvers map [ string ] solver
2015-06-08 00:36:07 +00:00
}
// NewClient creates a new client for the set user.
2015-06-13 01:55:53 +00:00
func NewClient ( caURL string , usr User , keyBits int , optPort string ) * Client {
2015-06-08 00:36:07 +00:00
if err := usr . GetPrivateKey ( ) . Validate ( ) ; err != nil {
2015-09-26 17:45:52 +00:00
logger ( ) . Fatalf ( "Could not validate the private account key of %s\n\t%v" , usr . GetEmail ( ) , err )
2015-06-08 00:36:07 +00:00
}
2015-06-11 22:13:43 +00:00
jws := & jws { privKey : usr . GetPrivateKey ( ) }
2015-06-10 23:11:14 +00:00
// REVIEW: best possibility?
2015-06-13 02:50:36 +00:00
// Add all available solvers with the right index as per ACME
// spec to this map. Otherwise they won`t be found.
2015-06-11 13:31:09 +00:00
solvers := make ( map [ string ] solver )
2015-09-26 17:45:52 +00:00
solvers [ "simpleHttp" ] = & simpleHTTPChallenge { jws : jws , optPort : optPort }
2015-06-10 23:11:14 +00:00
2015-09-26 20:59:16 +00:00
dirResp , err := http . Get ( caURL + "/directory" )
if err != nil {
logger ( ) . Fatalf ( "Could not get directory from CA URL. Please check the URL.\n\t%v" , err )
}
var dir directory
decoder := json . NewDecoder ( dirResp . Body )
err = decoder . Decode ( & dir )
if err != nil {
logger ( ) . Fatalf ( "Could not parse directory response from CA URL.\n\t%v" , err )
}
if dir . NewRegURL == "" || dir . NewAuthzURL == "" || dir . NewCertURL == "" || dir . RevokeCertURL == "" {
logger ( ) . Fatal ( "The directory returned by the server was invalid." )
}
return & Client { directory : dir , user : usr , jws : jws , keyBits : keyBits , solvers : solvers }
2015-06-08 00:36:07 +00:00
}
// Register the current account to the ACME server.
func ( c * Client ) Register ( ) ( * RegistrationResource , error ) {
logger ( ) . Print ( "Registering account ... " )
2015-09-26 17:45:52 +00:00
jsonBytes , err := json . Marshal ( registrationMessage { Resource : "new-reg" , Contact : [ ] string { "mailto:" + c . user . GetEmail ( ) } } )
2015-06-08 00:36:07 +00:00
if err != nil {
return nil , err
}
2015-09-26 20:59:16 +00:00
resp , err := c . jws . post ( c . directory . NewRegURL , jsonBytes )
2015-06-08 00:36:07 +00:00
if err != nil {
return nil , err
}
if resp . StatusCode == http . StatusConflict {
// REVIEW: should this return an error?
return nil , errors . New ( "This account is already registered with this CA." )
}
var serverReg Registration
decoder := json . NewDecoder ( resp . Body )
err = decoder . Decode ( & serverReg )
if err != nil {
return nil , err
}
reg := & RegistrationResource { Body : serverReg }
links := parseLinks ( resp . Header [ "Link" ] )
reg . URI = resp . Header . Get ( "Location" )
if links [ "terms-of-service" ] != "" {
reg . TosURL = links [ "terms-of-service" ]
}
if links [ "next" ] != "" {
reg . NewAuthzURL = links [ "next" ]
} else {
return nil , errors . New ( "The server did not return enough information to proceed..." )
}
return reg , nil
}
2015-06-08 21:54:15 +00:00
// AgreeToTos updates the Client registration and sends the agreement to
// the server.
func ( c * Client ) AgreeToTos ( ) error {
c . user . GetRegistration ( ) . Body . Agreement = c . user . GetRegistration ( ) . TosURL
2015-09-26 17:45:52 +00:00
c . user . GetRegistration ( ) . Body . Resource = "reg"
2015-06-08 21:54:15 +00:00
jsonBytes , err := json . Marshal ( & c . user . GetRegistration ( ) . Body )
if err != nil {
return err
}
2015-06-11 22:13:43 +00:00
resp , err := c . jws . post ( c . user . GetRegistration ( ) . URI , jsonBytes )
2015-06-08 21:54:15 +00:00
if err != nil {
return err
}
if resp . StatusCode != http . StatusAccepted {
return fmt . Errorf ( "The server returned %d but we expected %d" , resp . StatusCode , http . StatusAccepted )
}
return nil
}
// ObtainCertificates tries to obtain certificates from the CA server
2015-06-13 01:55:53 +00:00
// using the challenges it has configured. The returned certificates are
// DER encoded byte slices.
func ( c * Client ) ObtainCertificates ( domains [ ] string ) ( [ ] CertificateResource , error ) {
logger ( ) . Print ( "Obtaining certificates..." )
2015-06-10 13:11:01 +00:00
challenges := c . getChallenges ( domains )
2015-06-13 01:55:53 +00:00
err := c . solveChallenges ( challenges )
if err != nil {
return nil , err
}
logger ( ) . Print ( "Validations succeeded. Getting certificates" )
return c . requestCertificates ( challenges )
2015-06-10 13:11:01 +00:00
}
2015-09-27 12:51:44 +00:00
func ( c * Client ) RevokeCertificate ( certificate [ ] byte ) error {
encodedCert := base64 . URLEncoding . EncodeToString ( certificate )
jsonBytes , err := json . Marshal ( revokeCertMessage { Resource : "revoke-cert" , Certificate : encodedCert } )
if err != nil {
return err
}
resp , err := c . jws . post ( c . directory . RevokeCertURL , jsonBytes )
if err != nil {
return err
}
if resp . StatusCode != 200 {
body , _ := ioutil . ReadAll ( resp . Body )
return fmt . Errorf ( "The server returned an error while trying to revoke the certificate.\n%s" , body )
}
return nil
}
2015-06-10 23:11:14 +00:00
// Looks through the challenge combinations to find a solvable match.
// Then solves the challenges in series and returns.
2015-06-11 22:15:13 +00:00
func ( c * Client ) solveChallenges ( challenges [ ] * authorizationResource ) error {
2015-06-10 23:11:14 +00:00
// loop through the resources, basically through the domains.
2015-06-11 22:15:13 +00:00
for _ , authz := range challenges {
// no solvers - no solving
2015-06-13 16:37:30 +00:00
if solvers := c . chooseSolvers ( authz . Body , authz . Domain ) ; solvers != nil {
2015-06-11 22:15:13 +00:00
for i , solver := range solvers {
2015-06-13 01:55:53 +00:00
// TODO: do not immediately fail if one domain fails to validate.
err := solver . Solve ( authz . Body . Challenges [ i ] , authz . Domain )
if err != nil {
return err
}
2015-06-11 22:15:13 +00:00
}
} else {
return fmt . Errorf ( "Could not determine solvers for %s" , authz . Domain )
}
2015-06-10 13:11:01 +00:00
}
2015-06-11 22:15:13 +00:00
return nil
}
// Checks all combinations from the server and returns an array of
// solvers which should get executed in series.
2015-06-13 16:37:30 +00:00
func ( c * Client ) chooseSolvers ( auth authorization , domain string ) map [ int ] solver {
2015-06-11 22:15:13 +00:00
for _ , combination := range auth . Combinations {
solvers := make ( map [ int ] solver )
2015-06-13 01:55:53 +00:00
for _ , idx := range combination {
2015-06-14 00:33:21 +00:00
if solver , ok := c . solvers [ auth . Challenges [ idx ] . Type ] ; ok && solver . CanSolve ( domain ) {
2015-06-13 01:55:53 +00:00
solvers [ idx ] = solver
} else {
logger ( ) . Printf ( "Could not find solver for: %s" , auth . Challenges [ idx ] . Type )
2015-06-11 22:15:13 +00:00
}
}
// If we can solve the whole combination, return the solvers
if len ( solvers ) == len ( combination ) {
return solvers
}
}
return nil
2015-06-10 13:11:01 +00:00
}
2015-06-10 23:11:14 +00:00
// Get the challenges needed to proof our identifier to the ACME server.
2015-06-10 13:11:01 +00:00
func ( c * Client ) getChallenges ( domains [ ] string ) [ ] * authorizationResource {
2015-06-08 21:54:15 +00:00
resc , errc := make ( chan * authorizationResource ) , make ( chan error )
2015-06-10 13:11:01 +00:00
2015-06-08 21:54:15 +00:00
for _ , domain := range domains {
go func ( domain string ) {
2015-09-26 17:45:52 +00:00
jsonBytes , err := json . Marshal ( authorization { Resource : "new-authz" , Identifier : identifier { Type : "dns" , Value : domain } } )
2015-06-08 21:54:15 +00:00
if err != nil {
errc <- err
return
}
2015-06-11 22:13:43 +00:00
resp , err := c . jws . post ( c . user . GetRegistration ( ) . NewAuthzURL , jsonBytes )
2015-06-08 21:54:15 +00:00
if err != nil {
errc <- err
return
}
if resp . StatusCode != http . StatusCreated {
errc <- fmt . Errorf ( "Getting challenges for %s failed. Got status %d but expected %d" ,
domain , resp . StatusCode , http . StatusCreated )
}
links := parseLinks ( resp . Header [ "Link" ] )
if links [ "next" ] == "" {
logger ( ) . Fatalln ( "The server did not provide enough information to proceed." )
}
var authz authorization
decoder := json . NewDecoder ( resp . Body )
err = decoder . Decode ( & authz )
if err != nil {
errc <- err
}
2015-06-13 01:55:53 +00:00
resc <- & authorizationResource { Body : authz , NewCertURL : links [ "next" ] , AuthURL : resp . Header . Get ( "Location" ) , Domain : domain }
2015-06-08 21:54:15 +00:00
} ( domain )
}
var responses [ ] * authorizationResource
for i := 0 ; i < len ( domains ) ; i ++ {
select {
case res := <- resc :
responses = append ( responses , res )
case err := <- errc :
logger ( ) . Printf ( "%v" , err )
}
}
close ( resc )
close ( errc )
2015-06-10 13:11:01 +00:00
return responses
2015-06-08 21:54:15 +00:00
}
2015-06-13 02:50:36 +00:00
// requestCertificates iterates all granted authorizations, creates RSA private keys and CSRs.
// It then uses these to request a certificate from the CA and returns the list of successfully
// granted certificates.
2015-06-13 01:55:53 +00:00
func ( c * Client ) requestCertificates ( challenges [ ] * authorizationResource ) ( [ ] CertificateResource , error ) {
var certs [ ] CertificateResource
for _ , authz := range challenges {
privKey , err := generatePrivateKey ( c . keyBits )
if err != nil {
return nil , err
}
csr , err := generateCsr ( privKey , authz . Domain )
if err != nil {
return nil , err
}
csrString := base64 . URLEncoding . EncodeToString ( csr )
2015-09-26 17:45:52 +00:00
jsonBytes , err := json . Marshal ( csrMessage { Resource : "new-cert" , Csr : csrString , Authorizations : [ ] string { authz . AuthURL } } )
2015-06-13 01:55:53 +00:00
if err != nil {
return nil , err
}
resp , err := c . jws . post ( authz . NewCertURL , jsonBytes )
if err != nil {
return nil , err
}
if resp . Header . Get ( "Content-Type" ) != "application/pkix-cert" {
return nil , fmt . Errorf ( "The server returned an unexpected content-type header: %s - expected %s" , resp . Header . Get ( "Content-Type" ) , "application/pkix-cert" )
}
cert , err := ioutil . ReadAll ( resp . Body )
if err != nil {
return nil , err
}
privateKeyPem := pemEncode ( privKey )
certs = append ( certs , CertificateResource { Domain : authz . Domain , CertURL : resp . Header . Get ( "Location" ) , PrivateKey : privateKeyPem , Certificate : cert } )
}
return certs , nil
}
2015-06-08 00:36:07 +00:00
func logResponseHeaders ( resp * http . Response ) {
logger ( ) . Println ( resp . Status )
for k , v := range resp . Header {
logger ( ) . Printf ( "-- %s: %s" , k , v )
}
}
func logResponseBody ( resp * http . Response ) {
body , _ := ioutil . ReadAll ( resp . Body )
logger ( ) . Printf ( "Returned json data: \n%s" , body )
}
func parseLinks ( links [ ] string ) map [ string ] string {
aBrkt := regexp . MustCompile ( "[<>]" )
slver := regexp . MustCompile ( "(.+) *= *\"(.+)\"" )
linkMap := make ( map [ string ] string )
for _ , link := range links {
link = aBrkt . ReplaceAllString ( link , "" )
parts := strings . Split ( link , ";" )
matches := slver . FindStringSubmatch ( parts [ 1 ] )
if len ( matches ) > 0 {
linkMap [ matches [ 2 ] ] = parts [ 0 ]
}
}
return linkMap
}