Initial check-in
This commit is contained in:
parent
b45b86ddcc
commit
ea10849dcf
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
@ -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!
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue