Initial check-in

This commit is contained in:
Richard Barnes 2014-12-22 16:52:00 -05:00
parent b45b86ddcc
commit ea10849dcf
13 changed files with 2373 additions and 0 deletions

153
README.md Normal file
View File

@ -0,0 +1,153 @@
Anvil - An ACME CA
==================
This is an initial implementation of an ACME-based CA. The [ACME protocol](https://github.com/letsencrypt/acme-spec/) allows the CA to automatically verify that an applicant for a certificate actually controls an identifier, and allows a domain holder to issue and revoke certificates for his domains.
Quickstart
----------
```
> go get github.com/letsencrypt/anvil
> go build github.com/letsencrypt/anvil/anvil-start
> ./anvil-start monolithic # without AMQP
> ./anvil-start monolithic-amqp # with AMQP
```
The ["restify" branch of node-acme](https://github.com/letsencrypt/node-acme/tree/restify) has a client that works with this server (`npm install node-acme && node node-acme/demo.js`).
```
> git clone https://github.com/letsencrypt/node-acme.git
> cd node-acme
> git branch -f restify origin/restify && git checkout restify
> cd ..
> npm install node-acme
> node node-acme/demo.js
```
Component Model
---------------
The CA is divided into the following main components:
1. Web Front End
2. Registration Authority
3. Validation Authority
4. Certificate Authority
5. Storage Authority
This component model lets us separate the function of the CA by security context. The Web Front End and Validation Authority need access to the Internet, which puts them at greater risk of compromise. The Registration Authority can live without Internet connectivity, but still needs to talk to the Web Front End and Validation Authority. The Certificate Authority need only receive instructions from the Registration Authority.
```
client <--ACME--> WFE ---+
. |
. +--- RA --- CA
. |
client <-checks-> VA ---+
```
In Anvil, these components are represented by Go interfaces. This allows us to have two operational modes: Consolidated and distributed. In consolidated mode, the objects representing the different components interact directly, through function calls. In distributed mode, each component runs in a separate process (possibly on a separate machine), and sees the other components' methods by way of a messaging layer.
Internally, the logic of the system is based around two types of objects, authorizations and certificates, mapping directly to the resources of the same name in ACME.
Requests from ACME clients result in new objects and changes objects. The Storage Authority maintains persistent copies of the current set of objects.
Objects are also passed from one component to another on change events. For example, when a client provides a successful response to a validation challenge, it results in a change to the corresponding validation object. The Validation Authority forward the new validation object to the Storage Authority for storage, and to the Registration Authority for any updates to a related Authorization object.
Anvil supports distributed operation using AMQP as a message bus (e.g., via RabbitMQ). For components that you want to be remote, it is necessary to instantiate a "client" and "server" for that component. The client implements the component's Go interface, while the server has the actual logic for the component. More details in `amqp-rpc.go`.
Files
-----
* `interfaces.go` - Interfaces to the components, implemented in:
* `web-front-end.go`
* `registration-authority.go`
* `validation-authority.go`
* `certificate-authority.go`
* `storage-authority.go`
* `amqp-rpc.go` - A lightweight RPC framework overlaid on AMQP
* `rpc-wrappers.go` - RPC wrappers for the various component type
* `objects.go` - Objects that are passed between components
* `util.go` - Miscellaneous utility methods
* `anvil_test.go` - Unit tests
Dependencies:
* Go platform libraries
* [GOSE](https://github.com/bifurcation/gose)
* [CLI](github.com/codegangsta/cli)
ACME Processing
---------------
```
Client -> WebFE: challengeRequest
WebFE -> RA: NewAuthorization(AuthorizationRequest)
RA -> RA: [ select challenges ]
RA -> RA: [ create Validations with challenges ]
RA -> RA: [ create Authorization with Validations ]
RA -> SA: Update(Authorization.ID, Authorization)
RA -> WebFE: Authorization
WebFE -> WebFE: [ create challenge from Authorization ]
WebFE -> WebFE: [ generate nonce and add ]
WebFE -> Client: challenge
----------
Client -> WebFE: authorizationRequest
WebFE -> WebFE: [ look up authorization based on nonce ]
WebFE -> WebFE: [ verify authorization signature ]
WebFE -> RA: UpdateAuthorization(Authorization)
RA -> RA: [ add responses to authorization ]
RA -> SA: Update(Authorization.ID, Authorization)
WebFE -> VA: UpdateValidations(Authorization)
WebFE -> Client: defer(authorizationID)
VA -> SA: Update(Authorization.ID, Authorization)
VA -> RA: OnValidationUpdate(Authorization)
RA -> RA: [ check that validation sufficient ]
RA -> RA: [ finalize authorization ]
RA -> SA: Update(Authorization.ID, Authorization)
RA -> WebFE: OnAuthorizationUpdate(Authorization)
Client -> WebFE: statusRequest
WebFE -> Client: error / authorization
----------
Client -> WebFE: certificateRequest
WebFE -> WebFE: [ verify authorization signature ]
WebFE -> RA: NewCertificate(CertificateRequest)
RA -> RA: [ verify CSR signature ]
RA -> RA: [ verify authorization to issue ]
RA -> RA: [ select CA based on issuer ]
RA -> CA: IssueCertificate(CertificateRequest)
CA -> RA: Certificate
RA -> CA: [ look up ancillary data ]
RA -> WebFE: AcmeCertificate
WebFE -> Client: certificate
----------
Client -> WebFE: revocationRequest
WebFE -> WebFE: [ verify authorization signature ]
WebFE -> RA: RevokeCertificate(RevocationRequest)
RA -> RA: [ verify authorization ]
RA -> CA: RevokeCertificate(Certificate)
CA -> RA: RevocationResult
RA -> WebFE: RevocationResult
WebFE -> Client: revocation
```
TODO
----
* Ensure that distributed mode works with multiple processes
* Add message signing and verification to the AMQP message layer
* Add monitoring / syslog
* Factor out policy layer (e.g., selection of challenges)
* Add persistent storage

237
amqp-rpc.go Normal file
View File

@ -0,0 +1,237 @@
// Copyright 2014 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package anvil
import (
"errors"
"github.com/streadway/amqp"
"log"
"time"
)
// TODO: AMQP-RPC messages should be wrapped in JWS. To implement that,
// it will be necessary to make the following changes:
//
// * Constructors: Provision private key, acceptable public keys
// * After consume: Verify and discard JWS wrapper
// * Before publish: Add JWS wrapper
// General AMQP helpers
// XXX: I *think* these constants are appropriate.
// We will probably want to tweak these in the future.
const (
AmqpExchange = ""
AmqpDurable = false
AmqpDeleteUnused = false
AmqpExclusive = false
AmqpNoWait = false
AmqpNoLocal = false
AmqpAutoAck = true
AmqpMandatory = false
AmqpImmediate = false
)
// A simplified way to get a channel for a given AMQP server
func amqpConnect(url string) (ch *amqp.Channel, err error) {
conn, err := amqp.Dial(url)
if err != nil {
return
}
ch, err = conn.Channel()
return
}
// A simplified way to declare and subscribe to an AMQP queue
func amqpSubscribe(ch *amqp.Channel, name string) (msgs <-chan amqp.Delivery, err error) {
q, err := ch.QueueDeclare(
name,
AmqpDurable,
AmqpDeleteUnused,
AmqpExclusive,
AmqpNoWait,
nil)
if err != nil {
return
}
msgs, err = ch.Consume(
q.Name,
"",
AmqpAutoAck,
AmqpExclusive,
AmqpNoLocal,
AmqpNoWait,
nil)
return
}
// An AMQP-RPC Server listens on a specified queue within an AMQP channel.
// When messages arrive on that queue, it dispatches them based on type,
// and returns the response to the ReplyTo queue.
//
// To implement specific functionality, using code should use the Handle
// method to add specific actions.
type AmqpRpcServer struct {
serverQueue string
channel *amqp.Channel
dispatchTable map[string]func([]byte) []byte
}
// Create a new AMQP-RPC server on the given queue and channel.
// Note that you must call Start() to actually start the server
// listening for requests.
func NewAmqpRpcServer(serverQueue string, channel *amqp.Channel) *AmqpRpcServer {
return &AmqpRpcServer{
serverQueue: serverQueue,
channel: channel,
dispatchTable: make(map[string]func([]byte) []byte),
}
}
func (rpc *AmqpRpcServer) Handle(method string, handler func([]byte) []byte) {
rpc.dispatchTable[method] = handler
}
// Starts the AMQP-RPC server running in a separate thread.
// There is currently no Stop() method.
func (rpc *AmqpRpcServer) Start() (err error) {
msgs, err := amqpSubscribe(rpc.channel, rpc.serverQueue)
if err != nil {
return
}
go func() {
for msg := range msgs {
// XXX-JWS: jws.Verify(body)
cb, present := rpc.dispatchTable[msg.Type]
log.Printf(" [s<] received %s(%s) [%s]", msg.Type, b64enc(msg.Body), msg.CorrelationId)
if present {
response := cb(msg.Body)
log.Printf(" [s>] sending %s(%s) [%s]", msg.Type, b64enc(response), msg.CorrelationId)
rpc.channel.Publish(
AmqpExchange,
msg.ReplyTo,
AmqpMandatory,
AmqpImmediate,
amqp.Publishing{
CorrelationId: msg.CorrelationId,
Type: msg.Type,
Body: response, // XXX-JWS: jws.Sign(privKey, body)
})
}
}
}()
return
}
// An AMQP-RPC client sends requests to a specific server queue,
// and uses a dedicated response queue for responses.
//
// To implement specific functionality, using code uses the Dispatch()
// method to send a method name and body, and get back a response. So
// you end up with wrapper methods of the form:
//
// ```
// request = /* serialize request to []byte */
// response = <-AmqpRpcClient.Dispatch(method, request)
// return /* deserialized response */
// ```
//
// Callers that don't care about the response can just call Dispatch()
// and ignore the return value.
//
// DispatchSync will manage the channel for you, and also enforce a
// timeout on the transaction (default 60 seconds)
type AmqpRpcClient struct {
serverQueue string
clientQueue string
channel *amqp.Channel
pending map[string]chan []byte
timeout time.Duration
}
func NewAmqpRpcClient(clientQueue, serverQueue string, channel *amqp.Channel) (rpc *AmqpRpcClient, err error) {
rpc = &AmqpRpcClient{
serverQueue: serverQueue,
clientQueue: clientQueue,
channel: channel,
pending: make(map[string]chan []byte),
timeout: 10 * time.Second,
}
// Subscribe to the response queue and dispatch
msgs, err := amqpSubscribe(rpc.channel, clientQueue)
if err != nil {
return
}
go func() {
for msg := range msgs {
// XXX-JWS: jws.Sign(privKey, body)
corrID := msg.CorrelationId
responseChan, present := rpc.pending[corrID]
log.Printf(" [c<] received %s(%s) [%s]", msg.Type, b64enc(msg.Body), corrID)
if present {
responseChan <- msg.Body
delete(rpc.pending, corrID)
}
}
}()
return
}
func (rpc *AmqpRpcClient) SetTimeout(ttl time.Duration) {
rpc.timeout = ttl
}
func (rpc *AmqpRpcClient) Dispatch(method string, body []byte) chan []byte {
// Create a channel on which to direct the response
// At least in some cases, it's important that this channel
// be buffered to avoid deadlock
responseChan := make(chan []byte, 1)
corrID := newToken()
rpc.pending[corrID] = responseChan
// Send the request
log.Printf(" [c>] sending %s(%s) [%s]", method, b64enc(body), corrID)
rpc.channel.Publish(
AmqpExchange,
rpc.serverQueue,
AmqpMandatory,
AmqpImmediate,
amqp.Publishing{
CorrelationId: corrID,
ReplyTo: rpc.clientQueue,
Type: method,
Body: body, // XXX-JWS: jws.Sign(privKey, body)
})
return responseChan
}
func (rpc *AmqpRpcClient) DispatchSync(method string, body []byte) (response []byte, err error) {
select {
case response = <-rpc.Dispatch(method, body):
return
case <-time.After(rpc.timeout):
log.Printf(" [c!] AMQP-RPC timeout [%s]", method)
err = errors.New("AMQP-RPC timeout")
return
}
err = errors.New("Unknown error in SyncDispatch")
return
}
func (rpc *AmqpRpcClient) SyncDispatchWithTimeout(method string, body []byte, ttl time.Duration) (response []byte, err error) {
switch {
}
return
}

248
anvil-start/main.go Normal file
View File

@ -0,0 +1,248 @@
// Copyright 2014 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package main
import (
"fmt"
"github.com/codegangsta/cli"
"github.com/letsencrypt/anvil"
"github.com/streadway/amqp"
"net/http"
"os"
)
// Exit and print error message if we encountered a problem
func failOnError(err error, msg string) {
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %s\n", msg, err)
os.Exit(1)
}
}
// This is the same as amqpConnect in anvil, but with even
// more aggressive error dropping
func amqpChannel(url string) (ch *amqp.Channel) {
conn, err := amqp.Dial(url)
failOnError(err, "Unable to connect to AMQP server")
ch, err = conn.Channel()
failOnError(err, "Unable to establish channel to AMQP server")
return
}
// Start the server and wait around
func runForever(server *anvil.AmqpRpcServer) {
forever := make(chan bool)
server.Start()
fmt.Fprintf(os.Stderr, "Server running...\n")
<-forever
}
func main() {
app := cli.NewApp()
app.Name = "anvil-start"
app.Usage = "Command-line utility to start Anvil's servers in stand-alone mode"
app.Version = "0.0.0"
// Server URL hard-coded for now
amqpServerURL := "amqp://guest:guest@localhost:5672"
// One command per element of the system
// * WebFrontEnd
// * RegistrationAuthority
// * ValidationAuthority
// * CertificateAuthority
// * StorageAuthority
//
// Once started, we just run until killed
//
// AMQP queue names are hard-coded for now
app.Commands = []cli.Command{
{
Name: "monolithic",
Usage: "Start the CA in monolithic mode, without using AMQP",
Action: func(c *cli.Context) {
// Create the components
wfe := anvil.NewWebFrontEndImpl()
sa := anvil.NewSimpleStorageAuthorityImpl()
ra := anvil.NewRegistrationAuthorityImpl()
va := anvil.NewValidationAuthorityImpl()
ca, err := anvil.NewCertificateAuthorityImpl()
failOnError(err, "Unable to create CA")
// Wire them up
wfe.RA = &ra
wfe.SA = &sa
ra.CA = &ca
ra.SA = &sa
ra.VA = &va
va.RA = &ra
// Go!
authority := "localhost:4000"
authzPath := "/acme/authz/"
certPath := "/acme/cert/"
wfe.SetAuthzBase("http://" + authority + authzPath)
wfe.SetCertBase("http://" + authority + certPath)
http.HandleFunc("/acme/new-authz", wfe.NewAuthz)
http.HandleFunc("/acme/new-cert", wfe.NewCert)
http.HandleFunc("/acme/authz/", wfe.Authz)
http.HandleFunc("/acme/cert/", wfe.Cert)
fmt.Fprintf(os.Stderr, "Server running...\n")
err = http.ListenAndServe(authority, nil)
failOnError(err, "Error starting HTTP server")
},
},
{
Name: "monolithic-amqp",
Usage: "Start the CA in monolithic mode, using AMQP",
Action: func(c *cli.Context) {
// Create an AMQP channel
ch := amqpChannel(amqpServerURL)
// Create AMQP-RPC clients for CA, VA, RA, SA
cac, err := anvil.NewCertificateAuthorityClient("CA.client", "CA.server", ch)
failOnError(err, "Failed to create CA client")
vac, err := anvil.NewValidationAuthorityClient("VA.client", "VA.server", ch)
failOnError(err, "Failed to create VA client")
rac, err := anvil.NewRegistrationAuthorityClient("RA.client", "RA.server", ch)
failOnError(err, "Failed to create RA client")
sac, err := anvil.NewStorageAuthorityClient("SA.client", "SA.server", ch)
failOnError(err, "Failed to create SA client")
// ... and corresponding servers
// (We need this order so that we can give the servers
// references to the clients)
cas, err := anvil.NewCertificateAuthorityServer("CA.server", ch)
failOnError(err, "Failed to create CA server")
vas, err := anvil.NewValidationAuthorityServer("VA.server", ch, &rac)
failOnError(err, "Failed to create VA server")
ras, err := anvil.NewRegistrationAuthorityServer("RA.server", ch, &vac, &cac, &sac)
failOnError(err, "Failed to create RA server")
sas := anvil.NewStorageAuthorityServer("SA.server", ch)
// Start the servers
cas.Start()
vas.Start()
ras.Start()
sas.Start()
// Wire up the front end (wrappers are already wired)
wfe := anvil.NewWebFrontEndImpl()
wfe.RA = &rac
wfe.SA = &sac
// Go!
authority := "localhost:4000"
authzPath := "/acme/authz/"
certPath := "/acme/cert/"
wfe.SetAuthzBase("http://" + authority + authzPath)
wfe.SetCertBase("http://" + authority + certPath)
http.HandleFunc("/acme/new-authz", wfe.NewAuthz)
http.HandleFunc("/acme/new-cert", wfe.NewCert)
http.HandleFunc("/acme/authz/", wfe.Authz)
http.HandleFunc("/acme/cert/", wfe.Cert)
fmt.Fprintf(os.Stderr, "Server running...\n")
err = http.ListenAndServe(authority, nil)
failOnError(err, "Error starting HTTP server")
},
},
{
Name: "wfe",
Usage: "Start the WebFrontEnd",
Action: func(c *cli.Context) {
// Create necessary clients
ch := amqpChannel(amqpServerURL)
rac, err := anvil.NewRegistrationAuthorityClient("RA.client", "RA.server", ch)
failOnError(err, "Unable to create RA client")
sac, err := anvil.NewStorageAuthorityClient("SA.client", "SA.server", ch)
failOnError(err, "Unable to create SA client")
// Create the front-end and wire in its resources
wfe := anvil.NewWebFrontEndImpl()
wfe.RA = &rac
wfe.SA = &sac
// Connect the front end to HTTP
authority := "localhost:4000"
authzPath := "/acme/authz/"
certPath := "/acme/cert/"
wfe.SetAuthzBase("http://" + authority + authzPath)
wfe.SetCertBase("http://" + authority + certPath)
http.HandleFunc("/acme/new-authz", wfe.NewAuthz)
http.HandleFunc("/acme/new-cert", wfe.NewCert)
http.HandleFunc("/acme/authz/", wfe.Authz)
http.HandleFunc("/acme/cert/", wfe.Cert)
fmt.Fprintf(os.Stderr, "Server running...\n")
http.ListenAndServe(authority, nil)
},
},
{
Name: "sa",
Usage: "Start the CertificateAuthority",
Action: func(c *cli.Context) {
ch := amqpChannel(amqpServerURL)
cas, err := anvil.NewCertificateAuthorityServer("CA.server", ch)
failOnError(err, "Unable to create CA server")
runForever(cas)
},
},
{
Name: "ca",
Usage: "Start the StorageAuthority",
Action: func(c *cli.Context) {
ch := amqpChannel(amqpServerURL)
sas := anvil.NewStorageAuthorityServer("SA.server", ch)
runForever(sas)
},
},
{
Name: "va",
Usage: "Start the ValidationAuthority",
Action: func(c *cli.Context) {
ch := amqpChannel(amqpServerURL)
rac, err := anvil.NewRegistrationAuthorityClient("RA.client", "RA.server", ch)
failOnError(err, "Unable to create RA client")
vas, err := anvil.NewValidationAuthorityServer("VA.server", ch, &rac)
failOnError(err, "Unable to create VA server")
runForever(vas)
},
},
{
Name: "ra",
Usage: "Start the RegistrationAuthority",
Action: func(c *cli.Context) {
// TODO
ch := amqpChannel(amqpServerURL)
vac, err := anvil.NewValidationAuthorityClient("VA.client", "VA.server", ch)
failOnError(err, "Unable to create VA client")
cac, err := anvil.NewCertificateAuthorityClient("CA.client", "CA.server", ch)
failOnError(err, "Unable to create CA client")
sac, err := anvil.NewStorageAuthorityClient("SA.client", "SA.server", ch)
failOnError(err, "Unable to create SA client")
ras, err := anvil.NewRegistrationAuthorityServer("RA.server", ch, &vac, &cac, &sac)
failOnError(err, "Unable to create RA server")
runForever(ras)
},
},
}
err := app.Run(os.Args)
failOnError(err, "Failed to run application")
}

10
anvil_test.go Normal file
View File

@ -0,0 +1,10 @@
// Copyright 2014 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package anvil
import ()
// TODO: Unit tests!

122
certificate-authority.go Normal file
View File

@ -0,0 +1,122 @@
// Copyright 2014 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package anvil
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"errors"
"math/big"
"time"
)
type CertificateAuthorityImpl struct {
privateKey interface{}
certificate x509.Certificate
derCertificate []byte
}
var (
serialNumberBits = uint(64)
oneYear = 365 * 24 * time.Hour
rootCertificateTemplate = x509.Certificate{
SignatureAlgorithm: x509.SHA256WithRSA,
Subject: pkix.Name{Organization: []string{"ACME CA"}},
IsCA: true,
KeyUsage: x509.KeyUsageKeyEncipherment |
x509.KeyUsageDigitalSignature |
x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
eeCertificateTemplate = x509.Certificate{
SignatureAlgorithm: x509.SHA256WithRSA,
IsCA: false,
KeyUsage: x509.KeyUsageKeyEncipherment |
x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
)
func newSerialNumber() (*big.Int, error) {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), serialNumberBits)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil, err
}
return serialNumber, nil
}
func NewCertificateAuthorityImpl() (CertificateAuthorityImpl, error) {
zero := CertificateAuthorityImpl{}
// Generate a key pair
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return zero, err
}
// Sign the certificate
template := rootCertificateTemplate
template.SerialNumber, err = newSerialNumber()
if err != nil {
return zero, err
}
template.NotBefore = time.Now()
template.NotAfter = template.NotBefore.Add(oneYear)
der, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return zero, err
}
// Parse the certificate
certs, err := x509.ParseCertificates(der)
if err != nil || len(certs) == 0 {
return zero, err
}
return CertificateAuthorityImpl{
privateKey: priv,
certificate: *certs[0],
derCertificate: der,
}, nil
}
func (ca *CertificateAuthorityImpl) CACertificate() []byte {
return ca.derCertificate
}
func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest) ([]byte, error) {
template := eeCertificateTemplate
// Set serial
serialNumber, err := newSerialNumber()
if err != nil {
return nil, err
}
template.SerialNumber = serialNumber
// Set validity
template.NotBefore = time.Now()
template.NotAfter = template.NotBefore.Add(oneYear)
// Set hostnames
domains := csr.DNSNames
if len(csr.Subject.CommonName) > 0 {
domains = append(domains, csr.Subject.CommonName)
}
if len(domains) == 0 {
return []byte{}, errors.New("No names provided for certificate")
}
template.Subject = pkix.Name{CommonName: domains[0]}
template.DNSNames = domains
// Sign
return x509.CreateCertificate(rand.Reader, &template, &ca.certificate, csr.PublicKey, ca.privateKey)
}

86
interfaces.go Normal file
View File

@ -0,0 +1,86 @@
// Copyright 2014 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package anvil
import (
"crypto/x509"
"github.com/bifurcation/gose"
"net/http"
)
// A WebFrontEnd object supplies methods that can be hooked into
// the Go http module's server functions, principally http.HandleFunc()
//
// It also provides methods to configure the base for authorization and
// certificate URLs.
//
// It is assumed that the ACME server is laid out as follows:
// * One URL for new-authorization -> NewAuthz
// * One URL for new-certificate -> NewCert
// * One path for authorizations -> Authz
// * One path for certificates -> Cert
type WebFrontEnd interface {
// Set the base URL for authorizations
SetAuthzBase(path string)
// Set the base URL for certificates
SetCertBase(path string)
// This method represents the ACME new-authorization resource
NewAuthz(response http.ResponseWriter, request *http.Request)
// This method represents the ACME new-certificate resource
NewCert(response http.ResponseWriter, request *http.Request)
// Provide access to requests for authorization resources
Authz(response http.ResponseWriter, request *http.Request)
// Provide access to requests for authorization resources
Cert(response http.ResponseWriter, request *http.Request)
}
type RegistrationAuthority interface {
// [WebFrontEnd]
NewAuthorization(Authorization, jose.JsonWebKey) (Authorization, error)
// [WebFrontEnd]
NewCertificate(CertificateRequest, jose.JsonWebKey) (Certificate, error)
// [WebFrontEnd]
UpdateAuthorization(Authorization) (Authorization, error)
// [WebFrontEnd]
RevokeCertificate(x509.Certificate) error
// [ValidationAuthority]
OnValidationUpdate(Authorization)
}
type ValidationAuthority interface {
// [RegistrationAuthority]
UpdateValidations(Authorization) error
}
type CertificateAuthority interface {
// [RegistrationAuthority]
IssueCertificate(x509.CertificateRequest) ([]byte, error)
}
type StorageGetter interface {
Get(string) (interface{}, error)
}
type StorageUpdater interface {
Update(string, interface{}) error
}
// The StorageAuthority interface represnts a simple key/value
// store. It is divided into StorageGetter and StorageUpdater
// interfaces for privilege separation.
type StorageAuthority interface {
StorageGetter
StorageUpdater
}

181
objects.go Normal file
View File

@ -0,0 +1,181 @@
// Copyright 2014 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package anvil
import (
"crypto/x509"
"encoding/json"
"github.com/bifurcation/gose"
"time"
)
type IdentifierType string
type AcmeStatus string
type Buffer []byte
const (
StatusUnknown = AcmeStatus("unknown") // Unknown status; the default
StatusPending = AcmeStatus("pending") // In process; client has next action
StatusProcessing = AcmeStatus("processing") // In process; server has next action
StatusValid = AcmeStatus("valid") // Validation succeeded
StatusInvalid = AcmeStatus("invalid") // Validation failed
StatusRevoked = AcmeStatus("revoked") // Object no longer valid
)
const (
ChallengeTypeSimpleHTTPS = "simpleHttps"
ChallengeTypeDVSNI = "dvsni"
ChallengeTypeDNS = "dns"
ChallengeTypeRecoveryToken = "recoveryToken"
)
const (
IdentifierDNS = IdentifierType("dns")
)
// An AcmeIdentifier encodes an identifier that can
// be validated by ACME. The protocol allows for different
// types of identifier to be supported (DNS names, IP
// addresses, etc.), but currently anvil only supports
// domain names.
type AcmeIdentifier struct {
Type IdentifierType `json:"type"` // The type of identifier being encoded
Value string `json:"value"` // The identifier itself
}
// An ACME certificate request is just a CSR together with
// URIs pointing to authorizations that should collectively
// authorize the certificate being requsted.
//
// This type is never marshaled, since we only ever receive
// it from the client. So it carries some additional information
// that is useful internally. (We rely on Go's case-insensitive
// JSON unmarshal to properly unmarshal client requests.)
type CertificateRequest struct {
CSR *x509.CertificateRequest // The CSR
Authorizations []AcmeURL // Links to Authorization over the account key
}
type rawCertificateRequest struct {
CSR jose.JsonBuffer `json:"csr"` // The encoded CSR
Authorizations []AcmeURL `json:"authorizations"` // Authorizations
}
func (cr *CertificateRequest) UnmarshalJSON(data []byte) error {
var raw rawCertificateRequest
err := json.Unmarshal(data, &raw)
if err != nil {
return err
}
csr, err := x509.ParseCertificateRequest(raw.CSR)
if err != nil {
return err
}
cr.CSR = csr
cr.Authorizations = raw.Authorizations
return nil
}
func (cr CertificateRequest) MarshalJSON() ([]byte, error) {
return json.Marshal(rawCertificateRequest{
CSR: cr.CSR.Raw,
Authorizations: cr.Authorizations,
})
}
// Rather than define individual types for different types of
// challenge, we just throw all the elements into one bucket,
// together with the common metadata elements.
type Challenge struct {
// The status of this challenge
Status AcmeStatus `json:"status,omitempty"`
// If successful, the time at which this challenge
// was completed by the server.
Completed time.Time `json:"completed,omitempty"`
// Used by simpleHttps, recoveryToken, and dns challenges
Token string `json:"token,omitempty"`
// Used by simpleHttps challenges
Path string `json:"path,omitempty"`
// Used by dvsni challenges
R string `json:"r,omitempty"`
S string `json:"s,omitempty"`
Nonce string `json:"nonce,omitempty"`
}
// Merge a client-provide response to a challenge with the issued challenge
func (ch Challenge) MergeResponse(resp Challenge) Challenge {
// Only override fields that are supposed to be client-provided
if len(ch.Path) == 0 {
ch.Path = resp.Path
}
if len(ch.S) == 0 {
ch.S = resp.S
}
return ch
}
// An ACME authorization object represents the authorization
// of an account key holder to act on behalf of a domain. This
// struct is intended to be used both internally and for JSON
// marshaling on the wire. Any fields that should be suppressed
// on the wire (e.g., ID) must be made empty before marshaling.
type Authorization struct {
// An identifier for this authorization, unique across
// authorizations and certificates within this anvil instance.
ID string `json:"id,omitempty"`
// The identifier for which authorization is being given
Identifier AcmeIdentifier `json:"identifier,omitempty"`
// The account key that is authorized for the identifier
Key jose.JsonWebKey `json:"key,omitempty"`
// The status of the validation of this authorization
Status AcmeStatus `json:"status,omitempty"`
// The date after which this authorization will be no
// longer be considered valid
Expires time.Time `json:"expires,omitempty"`
// An array of challenges objects used to validate the
// applicant's control of the identifier. For authorizations
// in process, these are challenges to be fulfilled; for
// final authorizations, they describe the evidence that
// the server used in support of granting the authorization.
Challenges map[string]Challenge `json:"challenges,omitempty"`
// The server may suggest combinations of challenges if it
// requires more than one challenge to be completed.
Combinations [][]string `json:"combinations,omitempty"`
// The client may provide contact URIs to allow the server
// to push information to it.
Contact []AcmeURL `json:"contact,omitempty"`
}
// Certificate objects are entirely internal to Anvil. The only
// thing exposed on the wire is the certificate itself.
type Certificate struct {
// An identifier for this authorization, unique across
// authorizations and certificates within this anvil instance.
ID string
// The certificate itself
DER jose.JsonBuffer
// The revocation status of the certificate.
// * "valid" - not revoked
// * "revoked" - revoked
Status AcmeStatus
}

217
registration-authority.go Normal file
View File

@ -0,0 +1,217 @@
// Copyright 2014 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package anvil
import (
"crypto/sha256"
"crypto/x509"
"fmt"
"github.com/bifurcation/gose"
"regexp"
)
// All of the fields in RegistrationAuthorityImpl need to be
// populated, or there is a risk of panic.
type RegistrationAuthorityImpl struct {
CA CertificateAuthority
VA ValidationAuthority
SA StorageAuthority
}
func NewRegistrationAuthorityImpl() RegistrationAuthorityImpl {
return RegistrationAuthorityImpl{}
}
func forbiddenIdentifier(id string) bool {
// XXX Flesh this out, and add real policy. Only rough checks for now
// If it contains characters not allowed in a domain name ...
match, err := regexp.MatchString("[^a-zA-Z0-9.-]", id)
if (err != nil) || match {
return true
}
// If it is entirely numeric (like an IP address) ...
match, err = regexp.MatchString("[^0-9.]", id)
if (err != nil) || !match {
return true
}
return false
}
func fingerprint256(data []byte) string {
d := sha256.New()
d.Write(data)
return b64enc(d.Sum(nil))
}
func (ra *RegistrationAuthorityImpl) NewAuthorization(request Authorization, key jose.JsonWebKey) (Authorization, error) {
zero := Authorization{}
identifier := request.Identifier
// Check that the identifier is present and appropriate
if len(identifier.Value) == 0 {
return zero, MalformedRequestError("No identifier in authorization request")
} else if identifier.Type != IdentifierDNS {
return zero, NotSupportedError("Only domain validation is supported")
} else if forbiddenIdentifier(identifier.Value) {
return zero, UnauthorizedError("We will not authorize use of this identifier")
}
// Create validations
authID := newToken()
simpleHttps := SimpleHTTPSChallenge()
dvsni := DvsniChallenge()
// Create a new authorization object
authz := Authorization{
ID: authID,
Identifier: identifier,
Key: key,
Status: StatusPending,
Challenges: map[string]Challenge{
ChallengeTypeSimpleHTTPS: simpleHttps,
ChallengeTypeDVSNI: dvsni,
},
}
// Store the authorization object, then return it
err := ra.SA.Update(authz.ID, authz)
if err != nil {
return authz, err
}
return authz, nil
}
func (ra *RegistrationAuthorityImpl) NewCertificate(req CertificateRequest, jwk jose.JsonWebKey) (Certificate, error) {
csr := req.CSR
zero := Certificate{}
// Verify the CSR
// TODO: Verify that other aspects of the CSR are appropriate
err := VerifyCSR(csr)
if err != nil {
return zero, UnauthorizedError("Invalid signature on CSR")
}
// Get the authorized domain list for the authorization key
obj, err := ra.SA.Get(jwk.Thumbprint)
if err != nil {
return zero, UnauthorizedError("No authorized domains for this key")
}
domainSet := obj.(map[string]bool)
// Validate that authorization key is authorized for all domains
// TODO: Use linked authorizations?
names := csr.DNSNames
if len(csr.Subject.CommonName) > 0 {
names = append(names, csr.Subject.CommonName)
}
for _, name := range names {
if !domainSet[name] {
return zero, UnauthorizedError(fmt.Sprintf("Key not authorized for name %s", name))
}
}
// Create the certificate
cert, err := ra.CA.IssueCertificate(*csr)
if err != nil {
return zero, CertificateIssuanceError("Error issuing certificate")
}
// Identify the certificate object by the cert's SHA-256 fingerprint
certObj := Certificate{
ID: fingerprint256(cert),
DER: cert,
Status: StatusValid,
}
ra.SA.Update(certObj.ID, certObj)
return certObj, nil
}
func (ra *RegistrationAuthorityImpl) UpdateAuthorization(delta Authorization) (Authorization, error) {
zero := Authorization{}
// Fetch the copy of this authorization we have on file
obj, err := ra.SA.Get(delta.ID)
if err != nil {
return zero, err
}
authz := obj.(Authorization)
// Copy information over that the client is allowed to supply
if len(delta.Contact) > 0 {
authz.Contact = delta.Contact
}
newResponse := false
for t, challenge := range authz.Challenges {
response, present := delta.Challenges[t]
if !present {
continue
}
newResponse = true
authz.Challenges[t] = challenge.MergeResponse(response)
}
// Store the updated version
ra.SA.Update(authz.ID, authz)
// If any challenges were updated, dispatch to the VA for service
if newResponse {
ra.VA.UpdateValidations(authz)
}
return authz, nil
}
func (ra *RegistrationAuthorityImpl) RevokeCertificate(cert x509.Certificate) error {
// Attempt to fetch the corresponding certificate object
certID := fingerprint256(cert.Raw)
obj, err := ra.SA.Get(certID)
if err != nil {
return err
}
certObj := obj.(Certificate)
// Change the status and update the DB
certObj.Status = StatusInvalid
return ra.SA.Update(certID, certObj)
}
func (ra *RegistrationAuthorityImpl) OnValidationUpdate(authz Authorization) {
// Check to see whether the updated validations are sufficient
// Current policy is to accept if any validation succeeded
for _, val := range authz.Challenges {
if val.Status == StatusValid {
authz.Status = StatusValid
break
}
}
// If no validation succeeded, then the authorization is invalid
// NOTE: This only works because we only ever do one validation
if authz.Status != StatusValid {
authz.Status = StatusInvalid
}
ra.SA.Update(authz.ID, authz)
// Record a new domain/key binding, if authorized
if authz.Status == StatusValid {
var domainSet map[string]bool
obj, err := ra.SA.Get(authz.Key.Thumbprint)
if err != nil {
domainSet = make(map[string]bool)
} else {
domainSet = obj.(map[string]bool)
}
domainSet[authz.Identifier.Value] = true
ra.SA.Update(authz.Key.Thumbprint, domainSet)
}
}

475
rpc-wrappers.go Normal file
View File

@ -0,0 +1,475 @@
// Copyright 2014 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package anvil
import (
"crypto/x509"
"encoding/json"
"errors"
"github.com/bifurcation/gose"
"github.com/streadway/amqp"
"log"
)
// This file defines RPC wrappers around the ${ROLE}Impl classes,
// where ROLE covers:
// * RegistrationAuthority
// * ValidationAuthority
// * CertficateAuthority
// * StorageAuthority
//
// For each one of these, the are ${ROLE}Client and ${ROLE}Server
// types. ${ROLE}Server is to be run on the server side, as a more
// or less stand-alone component. ${ROLE}Client is loaded by the
// code making use of the functionality.
//
// The WebFrontEnd role does not expose any functionality over RPC,
// so it doesn't need wrappers.
const (
MethodNewAuthorization = "NewAuthorization" // RA
MethodNewCertificate = "NewCertificate" // RA
MethodUpdateAuthorization = "UpdateAuthorization" // RA
MethodRevokeCertificate = "RevokeCertificate" // RA
MethodOnValidationUpdate = "OnValidationUpdate" // RA
MethodUpdateValidations = "UpdateValidations" // VA
MethodIssueCertificate = "IssueCertificate" // CA
MethodGet = "Get" // SA
MethodUpdate = "Update" // SA
)
// RegistrationAuthorityClient / Server
// -> NewAuthorization
// -> NewCertificate
// -> UpdateAuthorization
// -> RevokeCertificate
// -> OnValidationUpdate
type authorizationRequest struct {
Authz Authorization
Key jose.JsonWebKey
}
type certificateRequest struct {
Req CertificateRequest
Key jose.JsonWebKey
}
func NewRegistrationAuthorityServer(serverQueue string, channel *amqp.Channel, va ValidationAuthority, ca CertificateAuthority, sa StorageAuthority) (rpc *AmqpRpcServer, err error) {
rpc = NewAmqpRpcServer(serverQueue, channel)
impl := NewRegistrationAuthorityImpl()
impl.VA = va
impl.CA = ca
impl.SA = sa
rpc.Handle(MethodNewAuthorization, func(req []byte) (response []byte) {
var ar authorizationRequest
err := json.Unmarshal(req, &ar)
if err != nil {
return
}
authz, err := impl.NewAuthorization(ar.Authz, ar.Key)
if err != nil {
return
}
response, err = json.Marshal(authz)
if err != nil {
response = []byte{}
}
return
})
rpc.Handle(MethodNewCertificate, func(req []byte) (response []byte) {
log.Printf(" [.] Entering MethodNewCertificate")
var cr certificateRequest
err := json.Unmarshal(req, &cr)
if err != nil {
log.Printf(" [!] Error unmarshaling certificate request: %s", err.Error())
log.Printf(" JSON data: %s", string(req))
return
}
log.Printf(" [.] No problem unmarshaling request")
cert, err := impl.NewCertificate(cr.Req, cr.Key)
if err != nil {
log.Printf(" [!] Error issuing new certificate: %s", err.Error())
return
}
log.Printf(" [.] No problem issuing new cert")
response, err = json.Marshal(cert)
if err != nil {
response = []byte{}
}
return
})
rpc.Handle(MethodUpdateAuthorization, func(req []byte) (response []byte) {
var authz Authorization
err := json.Unmarshal(req, &authz)
if err != nil {
return
}
newAuthz, err := impl.UpdateAuthorization(authz)
if err != nil {
return
}
response, err = json.Marshal(newAuthz)
if err != nil {
response = []byte{}
}
return
})
rpc.Handle(MethodRevokeCertificate, func(req []byte) (response []byte) {
// Nobody's listening, so it doesn't matter what we return
response = []byte{}
certs, err := x509.ParseCertificates(req)
if err != nil || len(certs) == 0 {
return
}
impl.RevokeCertificate(*certs[0])
return
})
rpc.Handle(MethodOnValidationUpdate, func(req []byte) (response []byte) {
// Nobody's listening, so it doesn't matter what we return
response = []byte{}
var authz Authorization
err := json.Unmarshal(req, &authz)
if err != nil {
return
}
impl.OnValidationUpdate(authz)
return
})
return rpc, nil
}
type RegistrationAuthorityClient struct {
rpc *AmqpRpcClient
}
func NewRegistrationAuthorityClient(clientQueue, serverQueue string, channel *amqp.Channel) (rac RegistrationAuthorityClient, err error) {
rpc, err := NewAmqpRpcClient(clientQueue, serverQueue, channel)
if err != nil {
return
}
rac = RegistrationAuthorityClient{rpc: rpc}
return
}
func (rac RegistrationAuthorityClient) NewAuthorization(authz Authorization, key jose.JsonWebKey) (newAuthz Authorization, err error) {
data, err := json.Marshal(authorizationRequest{authz, key})
if err != nil {
return
}
newAuthzData, err := rac.rpc.DispatchSync(MethodNewAuthorization, data)
if err != nil || len(newAuthzData) == 0 {
return
}
err = json.Unmarshal(newAuthzData, &newAuthz)
return
}
func (rac RegistrationAuthorityClient) NewCertificate(cr CertificateRequest, key jose.JsonWebKey) (cert Certificate, err error) {
data, err := json.Marshal(certificateRequest{cr, key})
if err != nil {
return
}
certData, err := rac.rpc.DispatchSync(MethodNewCertificate, data)
if err != nil || len(certData) == 0 {
return
}
err = json.Unmarshal(certData, &cert)
return
}
func (rac RegistrationAuthorityClient) UpdateAuthorization(authz Authorization) (newAuthz Authorization, err error) {
data, err := json.Marshal(authz)
if err != nil {
return
}
newAuthzData, err := rac.rpc.DispatchSync(MethodUpdateAuthorization, data)
if err != nil || len(newAuthzData) == 0 {
return
}
err = json.Unmarshal(newAuthzData, &newAuthz)
return
}
func (rac RegistrationAuthorityClient) RevokeCertificate(cert x509.Certificate) (err error) {
rac.rpc.Dispatch(MethodRevokeCertificate, cert.Raw)
return
}
func (rac RegistrationAuthorityClient) OnValidationUpdate(authz Authorization) {
data, err := json.Marshal(authz)
if err != nil {
return
}
rac.rpc.Dispatch(MethodOnValidationUpdate, data)
return
}
// ValidationAuthorityClient / Server
// -> UpdateValidations
func NewValidationAuthorityServer(serverQueue string, channel *amqp.Channel, ra RegistrationAuthority) (rpc *AmqpRpcServer, err error) {
rpc = NewAmqpRpcServer(serverQueue, channel)
impl := NewValidationAuthorityImpl()
impl.RA = ra
rpc.Handle(MethodUpdateValidations, func(req []byte) []byte {
// Nobody's listening, so it doesn't matter what we return
zero := []byte{}
var authz Authorization
err := json.Unmarshal(req, &authz)
if err != nil {
return zero
}
impl.UpdateValidations(authz)
return zero
})
return rpc, nil
}
type ValidationAuthorityClient struct {
rpc *AmqpRpcClient
}
func NewValidationAuthorityClient(clientQueue, serverQueue string, channel *amqp.Channel) (vac ValidationAuthorityClient, err error) {
rpc, err := NewAmqpRpcClient(clientQueue, serverQueue, channel)
if err != nil {
return
}
vac = ValidationAuthorityClient{rpc: rpc}
return
}
func (vac ValidationAuthorityClient) UpdateValidations(authz Authorization) error {
data, err := json.Marshal(authz)
if err != nil {
return err
}
vac.rpc.Dispatch(MethodUpdateValidations, data)
return nil
}
// CertificateAuthorityClient / Server
// -> IssueCertificate
func NewCertificateAuthorityServer(serverQueue string, channel *amqp.Channel) (rpc *AmqpRpcServer, err error) {
rpc = NewAmqpRpcServer(serverQueue, channel)
impl, err := NewCertificateAuthorityImpl()
if err != nil {
return
}
rpc.Handle(MethodIssueCertificate, func(req []byte) []byte {
zero := []byte{}
csr, err := x509.ParseCertificateRequest(req)
if err != nil {
return zero // XXX
}
cert, err := impl.IssueCertificate(*csr)
if err != nil {
return zero // XXX
}
return cert
})
return
}
type CertificateAuthorityClient struct {
rpc *AmqpRpcClient
}
func NewCertificateAuthorityClient(clientQueue, serverQueue string, channel *amqp.Channel) (cac CertificateAuthorityClient, err error) {
rpc, err := NewAmqpRpcClient(clientQueue, serverQueue, channel)
if err != nil {
return
}
cac = CertificateAuthorityClient{rpc: rpc}
return
}
func (cac CertificateAuthorityClient) IssueCertificate(csr x509.CertificateRequest) (cert []byte, err error) {
cert, err = cac.rpc.DispatchSync(MethodIssueCertificate, csr.Raw)
if len(cert) == 0 {
// TODO: Better error handling
return []byte{}, errors.New("RPC resulted in error")
}
return
}
// StorageAuthorityClient / Server
// This requires a little subtlety, due to the type ambiguity.
// Instead of storing the objects directly, we tag them on the way
// in with what type they are ("Authorization", "Certificate",
// "DomainSet"), and go ahead and marshal them to JSON. Then on
// the way out, we can unmarshal the JSON to the right type.
// -> Get
// -> Update
const (
RecordTypeError = iota
RecordTypeAuthorization
RecordTypeCertificate
RecordTypeDomainSet
)
type storageRecord struct {
Type int
ID string
Content string
}
func NewStorageAuthorityServer(serverQueue string, channel *amqp.Channel) (rpc *AmqpRpcServer) {
rpc = NewAmqpRpcServer(serverQueue, channel)
impl := NewSimpleStorageAuthorityImpl()
rpc.Handle(MethodGet, func(req []byte) (response []byte) {
id := string(req)
var record storageRecord
obj, err := impl.Get(id)
if err != nil {
record = storageRecord{RecordTypeError, id, err.Error()}
} else {
record = obj.(storageRecord)
}
response, _ = json.Marshal(record) // XXX ignoring error
return
})
rpc.Handle(MethodUpdate, func(req []byte) (response []byte) {
var record storageRecord
err := json.Unmarshal(req, &record)
if err != nil {
return
}
err = impl.Update(record.ID, record)
return
})
return
}
type StorageAuthorityClient struct {
rpc *AmqpRpcClient
}
func NewStorageAuthorityClient(clientQueue, serverQueue string, channel *amqp.Channel) (sac StorageAuthorityClient, err error) {
rpc, err := NewAmqpRpcClient(clientQueue, serverQueue, channel)
if err != nil {
return
}
sac = StorageAuthorityClient{rpc: rpc}
return
}
func (cac StorageAuthorityClient) Update(token string, object interface{}) (err error) {
jsonText, err := json.Marshal(object)
if err != nil {
return
}
// Create a storage record
recordType := RecordTypeError
switch object.(type) {
case Authorization:
recordType = RecordTypeAuthorization
case Certificate:
recordType = RecordTypeCertificate
case map[string]bool:
recordType = RecordTypeDomainSet
default:
err = errors.New("I can't serialize that!")
return
}
jsonRecord, err := json.Marshal(storageRecord{recordType, token, string(jsonText)})
if err != nil {
return
}
// XXX Let's assume no real errors happen, and just fire and forget
_, err = cac.rpc.DispatchSync(MethodUpdate, jsonRecord)
return
}
func (cac StorageAuthorityClient) Get(token string) (object interface{}, err error) {
binaryRecord, err := cac.rpc.DispatchSync(MethodGet, []byte(token))
if err != nil {
return
}
var record storageRecord
err = json.Unmarshal(binaryRecord, &record)
if err != nil {
return
}
switch record.Type {
case RecordTypeError:
err = errors.New(record.Content)
return
case RecordTypeAuthorization:
var authz Authorization
err = json.Unmarshal([]byte(record.Content), &authz)
if err == nil {
object = authz
}
return
case RecordTypeCertificate:
var cert Certificate
err = json.Unmarshal([]byte(record.Content), &cert)
if err == nil {
object = cert
}
return
case RecordTypeDomainSet:
var domainSet map[string]bool
err = json.Unmarshal([]byte(record.Content), &domainSet)
if err == nil {
object = domainSet
}
return
}
// assert(false) // we should not get here
err = errors.New("I can't serialize that!")
return
}

38
storage-authority.go Normal file
View File

@ -0,0 +1,38 @@
// Copyright 2014 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package anvil
import (
"fmt"
)
type SimpleStorageAuthorityImpl struct {
Storage map[string]interface{}
}
func (sa *SimpleStorageAuthorityImpl) dumpState() {
fmt.Printf("Storage state: \n%+v\n", sa.Storage)
}
func NewSimpleStorageAuthorityImpl() SimpleStorageAuthorityImpl {
return SimpleStorageAuthorityImpl{
Storage: make(map[string]interface{}),
}
}
func (sa *SimpleStorageAuthorityImpl) Update(token string, object interface{}) error {
sa.Storage[token] = object
return nil
}
func (sa *SimpleStorageAuthorityImpl) Get(token string) (interface{}, error) {
value, ok := sa.Storage[token]
if ok {
return value, nil
} else {
return struct{}{}, NotFoundError("Unknown storage token")
}
}

159
util.go Normal file
View File

@ -0,0 +1,159 @@
// Copyright 2014 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package anvil
import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"hash"
"math/big"
"net/url"
"strings"
)
// Errors
type NotSupportedError string
type MalformedRequestError string
type UnauthorizedError string
type NotFoundError string
type SyntaxError string
type SignatureValidationError string
type CertificateIssuanceError string
func (e NotSupportedError) Error() string { return string(e) }
func (e MalformedRequestError) Error() string { return string(e) }
func (e UnauthorizedError) Error() string { return string(e) }
func (e NotFoundError) Error() string { return string(e) }
func (e SyntaxError) Error() string { return string(e) }
func (e SignatureValidationError) Error() string { return string(e) }
func (e CertificateIssuanceError) Error() string { return string(e) }
// Base64 functions
func pad(x string) string {
switch len(x) % 4 {
case 2:
return x + "=="
case 3:
return x + "="
}
return x
}
func unpad(x string) string {
return strings.Replace(x, "=", "", -1)
}
func b64enc(x []byte) string {
return unpad(base64.URLEncoding.EncodeToString(x))
}
func b64dec(x string) ([]byte, error) {
return base64.URLEncoding.DecodeString(pad(x))
}
// Random stuff
func randomString(length int) string {
b := make([]byte, length)
rand.Read(b) // NOTE: Ignoring errors
return b64enc(b)
}
func newToken() string {
return randomString(32)
}
// URLs that automatically marshal/unmarshal to JSON strings
type AcmeURL url.URL
func (u *AcmeURL) MarshalJSON() ([]byte, error) {
uu := url.URL(*u)
return json.Marshal(uu.String())
}
func (u *AcmeURL) UnmarshalJSON(data []byte) error {
var str string
err := json.Unmarshal(data, &str)
if err != nil {
return err
}
uu, err := url.Parse(str)
*u = AcmeURL(*uu)
return err
}
// The missing CertificateRequest.Verify() method
func VerifyCSR(csr *x509.CertificateRequest) error {
// Compute the hash of the TBSCertificateRequest
var hashID crypto.Hash
var hash hash.Hash
switch csr.SignatureAlgorithm {
case x509.SHA1WithRSA:
hashID = crypto.SHA1
hash = sha1.New()
case x509.SHA256WithRSA:
fallthrough
case x509.ECDSAWithSHA256:
hashID = crypto.SHA256
hash = sha256.New()
case x509.SHA384WithRSA:
fallthrough
case x509.ECDSAWithSHA384:
hashID = crypto.SHA384
hash = sha512.New384()
case x509.SHA512WithRSA:
fallthrough
case x509.ECDSAWithSHA512:
hashID = crypto.SHA512
hash = sha512.New()
default:
return errors.New("Unsupported CSR signing algorithm")
}
hash.Write(csr.RawTBSCertificateRequest)
inputHash := hash.Sum(nil)
// Verify the signature using the public key in the CSR
switch csr.SignatureAlgorithm {
case x509.SHA1WithRSA:
fallthrough
case x509.SHA256WithRSA:
fallthrough
case x509.SHA384WithRSA:
fallthrough
case x509.SHA512WithRSA:
rsaKey := csr.PublicKey.(*rsa.PublicKey)
return rsa.VerifyPKCS1v15(rsaKey, hashID, inputHash, csr.Signature)
case x509.ECDSAWithSHA256:
fallthrough
case x509.ECDSAWithSHA384:
fallthrough
case x509.ECDSAWithSHA512:
ecKey := csr.PublicKey.(*ecdsa.PublicKey)
intlen := len(csr.Signature) / 2
r, s := big.NewInt(0), big.NewInt(0)
r.SetBytes(csr.Signature[:intlen])
s.SetBytes(csr.Signature[intlen:])
if ecdsa.Verify(ecKey, inputHash, r, s) {
return nil
} else {
return errors.New("Invalid ECDSA signature on CSR")
}
}
return errors.New("Unsupported CSR signing algorithm")
}

177
validation-authority.go Normal file
View File

@ -0,0 +1,177 @@
// Copyright 2014 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package anvil
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"fmt"
"io/ioutil"
"net/http"
"time"
)
type ValidationAuthorityImpl struct {
RA RegistrationAuthority
}
func NewValidationAuthorityImpl() ValidationAuthorityImpl {
return ValidationAuthorityImpl{}
}
// Challenge factories
func SimpleHTTPSChallenge() Challenge {
return Challenge{
Status: StatusPending,
Token: newToken(),
}
}
func DvsniChallenge() Challenge {
nonce := make([]byte, 16)
rand.Read(nonce)
return Challenge{
Status: StatusPending,
R: randomString(32),
Nonce: hex.EncodeToString(nonce),
}
}
// Validation methods
func (va ValidationAuthorityImpl) validateSimpleHTTPS(authz Authorization) (challenge Challenge) {
identifier := authz.Identifier.Value
challenge, ok := authz.Challenges[ChallengeTypeSimpleHTTPS]
if !ok {
challenge.Status = StatusInvalid
return
}
if len(challenge.Path) == 0 {
challenge.Status = StatusInvalid
return
}
// XXX: Local version; uncomment for real version
url := fmt.Sprintf("http://localhost:5001/.well-known/acme-challenge/%s", challenge.Path)
//url := fmt.Sprintf("https://%s/.well-known/acme-challenge/%s", identifier, challenge.Path)
httpRequest, err := http.NewRequest("GET", url, nil)
if err != nil {
challenge.Status = StatusInvalid
return
}
httpRequest.Host = identifier
client := http.Client{Timeout: 5 * time.Second}
httpResponse, err := client.Do(httpRequest)
if err == nil && httpResponse.StatusCode == 200 {
// Read body & test
body, err := ioutil.ReadAll(httpResponse.Body)
if err != nil {
challenge.Status = StatusInvalid
return
}
if bytes.Compare(body, []byte(challenge.Token)) == 0 {
challenge.Status = StatusValid
return
}
}
challenge.Status = StatusInvalid
return
}
func (va ValidationAuthorityImpl) validateDvsni(authz Authorization) (challenge Challenge) {
// identifier := authz.Identifier.Value // XXX: Local version; uncomment for real version
challenge, ok := authz.Challenges[ChallengeTypeDVSNI]
if !ok {
challenge.Status = StatusInvalid
return
}
const DVSNI_SUFFIX = ".acme.invalid"
nonceName := challenge.Nonce + DVSNI_SUFFIX
R, err := b64dec(challenge.R)
if err != nil {
challenge.Status = StatusInvalid
return
}
S, err := b64dec(challenge.S)
if err != nil {
challenge.Status = StatusInvalid
return
}
RS := append(R, S...)
sha := sha256.New()
sha.Write(RS)
z := make([]byte, sha.Size())
sha.Sum(z)
zName := hex.EncodeToString(z)
// Make a connection with SNI = nonceName
hostPort := "localhost:5001"
//hostPort := identifier + ":443" // XXX: Local version; uncomment for real version
conn, err := tls.Dial("tcp", hostPort, &tls.Config{
ServerName: nonceName,
InsecureSkipVerify: true,
})
if err != nil {
challenge.Status = StatusInvalid
return
}
// Check that zName is a dNSName SAN in the server's certificate
certs := conn.ConnectionState().PeerCertificates
if len(certs) == 0 {
challenge.Status = StatusInvalid
return
}
for _, name := range certs[0].DNSNames {
if name == zName {
challenge.Status = StatusValid
return
}
}
challenge.Status = StatusInvalid
return
}
// Overall validation process
func (va ValidationAuthorityImpl) validate(authz Authorization) {
// Select the first supported validation method
// XXX: Remove the "break" lines to process all supported validations
for i := range authz.Challenges {
switch i {
case "simpleHttps":
authz.Challenges[i] = va.validateSimpleHTTPS(authz)
break
case "dvsni":
authz.Challenges[i] = va.validateDvsni(authz)
break
}
}
va.RA.OnValidationUpdate(authz)
}
func (va ValidationAuthorityImpl) UpdateValidations(authz Authorization) error {
go va.validate(authz)
return nil
}

270
web-front-end.go Normal file
View File

@ -0,0 +1,270 @@
// Copyright 2014 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package anvil
import (
"encoding/json"
"fmt"
"github.com/bifurcation/gose"
"io/ioutil"
"net/http"
"regexp"
)
type WebFrontEndImpl struct {
RA RegistrationAuthority
SA StorageGetter
// URL configuration parameters
baseURL string
authzBase string
certBase string
}
func NewWebFrontEndImpl() WebFrontEndImpl {
return WebFrontEndImpl{}
}
// Method implementations
func verifyPOST(request *http.Request) ([]byte, jose.JsonWebKey, error) {
zero := []byte{}
zeroKey := jose.JsonWebKey{}
// Read body
body, err := ioutil.ReadAll(request.Body)
if err != nil {
return zero, zeroKey, err
}
// Parse as JWS
var jws jose.JsonWebSignature
err = json.Unmarshal(body, &jws)
if err != nil {
return zero, zeroKey, err
}
// Verify JWS
// NOTE: It might seem insecure for the WFE to be trusted to verify
// client requests, i.e., that the verification should be done at the
// RA. However the WFE is the RA's only view of the outside world
// *anyway*, so it could always lie about what key was used by faking
// the signature itself.
err = jws.Verify()
if err != nil {
return zero, zeroKey, err
}
// TODO Return JWS body
return []byte(jws.Payload), jws.Header.Key, nil
}
// The ID is always the last slash-separated token in the path
func parseIDFromPath(path string) string {
re := regexp.MustCompile("^.*/")
return re.ReplaceAllString(path, "")
}
// Problem objects represent problem documents, which are
// returned with HTTP error responses
// https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
type problem struct {
Type string `json:"type,omitempty"`
Detail string `json:"detail,omitempty"`
Instance string `json:"instance,omitempty"`
}
func sendError(response http.ResponseWriter, message string, code int) {
problem := problem{Detail: message}
problemDoc, err := json.Marshal(problem)
if err != nil {
return
}
http.Error(response, string(problemDoc), code)
}
func (wfe *WebFrontEndImpl) SetAuthzBase(base string) {
wfe.authzBase = base
}
func (wfe *WebFrontEndImpl) SetCertBase(base string) {
wfe.certBase = base
}
func (wfe *WebFrontEndImpl) NewAuthz(response http.ResponseWriter, request *http.Request) {
if request.Method != "POST" {
sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
return
}
body, key, err := verifyPOST(request)
if err != nil {
sendError(response, "Unable to read body", http.StatusBadRequest)
return
}
var init Authorization
err = json.Unmarshal(body, &init)
if err != nil {
sendError(response, "Error unmarshaling JSON", http.StatusBadRequest)
return
}
// TODO: Create new authz and return
authz, err := wfe.RA.NewAuthorization(init, key)
if err != nil {
sendError(response,
fmt.Sprintf("Error creating new authz: %+v", err),
http.StatusInternalServerError)
return
}
// Make a URL for this authz, then blow away the ID before serializing
authzURL := wfe.authzBase + string(authz.ID)
authz.ID = ""
responseBody, err := json.Marshal(authz)
if err != nil {
sendError(response, "Error marshaling authz", http.StatusInternalServerError)
return
}
response.Header().Add("Location", authzURL)
response.WriteHeader(http.StatusCreated)
response.Write(responseBody)
}
func (wfe *WebFrontEndImpl) NewCert(response http.ResponseWriter, request *http.Request) {
if request.Method != "POST" {
sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
return
}
body, key, err := verifyPOST(request)
if err != nil {
sendError(response, "Unable to read body", http.StatusBadRequest)
return
}
var init CertificateRequest
err = json.Unmarshal(body, &init)
if err != nil {
sendError(response, "Error unmarshaling certificate request", http.StatusBadRequest)
return
}
// Create new certificate and return
cert, err := wfe.RA.NewCertificate(init, key)
if err != nil {
sendError(response,
fmt.Sprintf("Error creating new cert: %+v", err),
http.StatusBadRequest)
return
}
// Make a URL for this authz
certURL := wfe.certBase + string(cert.ID)
// TODO: Content negotiation for cert format
response.Header().Add("Location", certURL)
response.WriteHeader(http.StatusCreated)
response.Write(cert.DER)
}
func (wfe *WebFrontEndImpl) Authz(response http.ResponseWriter, request *http.Request) {
// Requests to this handler should have a path that leads to a known authz
id := parseIDFromPath(request.URL.Path)
obj, err := wfe.SA.Get(id)
if err != nil {
sendError(response,
fmt.Sprintf("Unable to find authorization: %+v", err),
http.StatusNotFound)
return
}
authz := obj.(Authorization)
switch request.Method {
default:
sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
return
case "POST":
body, key, err := verifyPOST(request)
if err != nil {
sendError(response, "Unable to read body", http.StatusBadRequest)
return
}
var initialAuthz Authorization
err = json.Unmarshal(body, &initialAuthz)
if err != nil {
sendError(response, "Error unmarshaling authorization", http.StatusBadRequest)
return
}
// Check that the signing key is the right key
if !key.Equals(authz.Key) {
fmt.Printf("req: %+v\n", key)
fmt.Printf("authz: %+v\n", authz.Key)
sendError(response, "Signing key does not match key in authorization", http.StatusForbidden)
return
}
// Ask the RA to update this authorization
initialAuthz.ID = authz.ID
updatedAuthz, err := wfe.RA.UpdateAuthorization(initialAuthz)
if err != nil {
sendError(response, "Unable to update authorization", http.StatusInternalServerError)
return
}
jsonReply, err := json.Marshal(updatedAuthz)
if err != nil {
sendError(response, "Failed to marshal authz", http.StatusInternalServerError)
return
}
response.WriteHeader(http.StatusAccepted)
response.Write(jsonReply)
case "GET":
jsonReply, err := json.Marshal(authz)
if err != nil {
sendError(response, "Failed to marshal authz", http.StatusInternalServerError)
return
}
response.WriteHeader(http.StatusOK)
response.Write(jsonReply)
}
}
func (wfe *WebFrontEndImpl) Cert(response http.ResponseWriter, request *http.Request) {
switch request.Method {
default:
sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
return
case "GET":
id := parseIDFromPath(request.URL.Path)
obj, err := wfe.SA.Get(id)
if err != nil {
sendError(response, "Not found", http.StatusNotFound)
return
}
cert := obj.(Certificate)
// TODO: Content negotiation
// TODO: Link header
jsonReply, err := json.Marshal(cert)
if err != nil {
sendError(response, "Failed to marshal cert", http.StatusInternalServerError)
return
}
response.WriteHeader(http.StatusOK)
response.Write(jsonReply)
case "POST":
// TODO: Handle revocation in POST
}
}