Merge pull request #35 from bifurcation/spec-update

Updating to be compatible with current acme-spec.
This commit is contained in:
bifurcation 2015-03-17 11:48:58 -04:00
commit 5a268b4149
18 changed files with 1766 additions and 235 deletions

View File

@ -34,8 +34,9 @@ type CertificateAuthorityImpl struct {
func NewCertificateAuthorityImpl(hostport string, authKey string, profile string) (ca *CertificateAuthorityImpl, err error) {
// Create the remote signer
localProfile := config.SigningProfile{
Expiry: 60 * time.Minute, // BOGUS: Required by CFSSL, but not used
RemoteName: hostport,
Expiry: time.Hour, // BOGUS: Required by CFSSL, but not used
RemoteName: hostport, // BOGUS: Only used as a flag by CFSSL
RemoteServer: hostport,
}
localProfile.Provider, err = auth.New(authKey, nil)
@ -56,15 +57,18 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest
// XXX Take in authorizations and verify that union covers CSR?
// Pull hostnames from CSR
hostNames := csr.DNSNames // DNSNames + CN from CSR
if len(hostNames) < 1 {
err = errors.New("Cannot issue a certificate without a hostname.")
return
}
var commonName string
if len(csr.Subject.CommonName) > 0 {
commonName = csr.Subject.CommonName
} else {
} else if len(hostNames) > 0 {
commonName = hostNames[0]
} else {
err = errors.New("Cannot issue a certificate without a hostname.")
return
}
if len(hostNames) == 0 {
hostNames = []string{commonName}
}
// Convert the CSR to PEM

View File

@ -7,10 +7,16 @@ package ca
import (
"crypto/x509"
"encoding/asn1"
"encoding/hex"
"encoding/pem"
"net/http"
"testing"
"time"
apisign "github.com/cloudflare/cfssl/api/sign"
"github.com/cloudflare/cfssl/auth"
"github.com/cloudflare/cfssl/config"
"github.com/cloudflare/cfssl/signer/local"
_ "github.com/mattn/go-sqlite3"
@ -108,24 +114,109 @@ var CA_CERT_PEM = "-----BEGIN CERTIFICATE-----\n" +
// * Random public key
// * CN = example.com
// * DNSNames = example.com, www.example.com
var CSR_HEX = "308202953082017d0201003016311430120603" +
"550403130b6578616d706c652e636f6d30820122300d06092a864886f70d0101010500038201" +
"0f003082010a0282010100baaf16e891828470cad87b849a73356f65e20ad3699fd5583a7200" +
"e924512d9eeb1dbe16441ad7bd804fa2e5726a06f0af5279012fe6354a5677259f5591984aa9" +
"99b8ea3ea10fbd5ecfa30e5f563b41c419374decfc98ea62c611046ad011c326470a2426f46d" +
"be6cc44fae3b247e19710810585f9f3ad7f64b2f305aebb72e2829866f89b20b03a300b7ff5f" +
"4e6204f41420d9fa731252692cee8e616636723abe8a7053fd86e2673190fa8b618ada5bc735" +
"ba57a145af86904a8f55a288d4d6ba9e501530f23f197f5b623443193fc92b7f87d6abbf740d" +
"9fc92800c7e0e1484d5eec6ffae1007c139c1ec19d67e749743fe8d8396fe190cfbcf2f90e05" +
"230203010001a03a303806092a864886f70d01090e312b302930270603551d110420301e820b" +
"6578616d706c652e636f6d820f7777772e6578616d706c652e636f6d300d06092a864886f70d" +
"01010b05000382010100514c622dc866b31c86777a26e9b2911618ce5b188591f08007b42772" +
"3497b733676a7d493c434fc819b8089869442fd299aa99ff7f7b9df881843727f0c8b89ca62a" +
"f8a12b38c963e9210148c4e1c0fc964aef2605f88ed777e6497d2e43e9a9713835d1ae96260c" +
"ca826c34c7ae52c77f5d8468643ee1936eadf04e1ebf8bbbb68b0ec7d0ef694432451292e4a3" +
"1989fd8339c07e01e04b6dd3834872b828d3f5b2b4dadda0596396e15fbdf446998066a74873" +
"2baf53f3f7ebb849e83cf734753c35ab454d1b62e1741a6514c5c575c0c805b4d668fcf71746" +
"ef32017613a52d6b807e2977f4fbc0a88b2e263347c4d9e35435333cf4f8288be53df41228ec"
var CN_AND_SAN_CSR_HEX = "308202a130820189020100301a311830160603550403130f6e6f742d6578" +
"616d706c652e636f6d30820122300d06092a864886f70d01010105000382" +
"010f003082010a0282010100e56ccbe37003c150202e6f543f9eb1d0e590" +
"76ac7f1f62654fa82fe131a23c66bd53a2f62ff7852015c84a394e36836d" +
"2018eba278e0740c85c4c6102787400c2ef069b4a72e6eb8ad8d1da5d76b" +
"f3e70dafc126578ed28cf40030e7fe5b5307ef630254726c639561b5445d" +
"372847bdb02576aa3622a688158c6af09d3938dbeba4d670cec4325be73a" +
"fa52a0a04dcba2f335f1e85020704db94ca125dce70b3209294c6c46ed4b" +
"48b95d8d51ae2d2fd227116023a48ca7381e35fd302ad2999df625a4b5ee" +
"82a0d0fefa88ac6a62b01674de75637ef83328202cda9930947d932000b0" +
"e53b82e099ab60fec9c8b6d4eccdee508b6ebca7e6ca3f752046c8350203" +
"010001a042304006092a864886f70d01090e31333031302f0603551d1104" +
"283026820f6e6f742d6578616d706c652e636f6d82137777772e6e6f742d" +
"6578616d706c652e636f6d300d06092a864886f70d01010b050003820101" +
"008c4bf2ab4dfd28d768697eecc5be889a6275287c7dd24f9232ffad5675" +
"de708c9cc911545d0e84f61b6584c5e237915bbf231d6518e7e228be2e65" +
"b4d50bd9729ce9e6aee00482e014de4edd4b9a4f9a7777b8943ef3512dbf" +
"940ac561c25b34ded9db1074136b978a65943ab1259608fb8109e008eac6" +
"23d7b29b2f1fad3a8e358aa070ead688016d9efed6da43412b136903de07" +
"137462d3f9203a344d84d7eb336999004e7e9972d5176001e2792f206e6c" +
"7c70b86d312459f21751d29ea53b41f9d02a229f9d7615b2a7ac83e849d0" +
"d0d9f8a08f8d7ba23295e77c95bc060c9227bfec0afb8c898e33c89903d7" +
"bbde4cf059dcc3e6c4ae4eef207c499d62"
// CSR generated by Go:
// * Random public key
// * CN = not-example.com
// * DNSNames = [none]
var NO_SAN_CSR_HEX = "3082025f30820147020100301a311830160603550403130f6e6f742d6578" +
"616d706c652e636f6d30820122300d06092a864886f70d01010105000382" +
"010f003082010a0282010100aa6e56ff24906f93b855e7871dc8411a3cf7" +
"678d9563627e8ca37ab17dfe814ef7f828d6aa92b717f0da9df56990b953" +
"989d5afc3f2dddacd2b504b89782b49e55a04a64a4370d8ab1b2688f2596" +
"98132e5ce536f812ef5eb13824a922bbb89e30d6f2cace77462b9e65264a" +
"32320a7b348f9903b16640bc8c1c5f1208c6b456fd85bfa96ee9b7642c68" +
"3ab05b142d249525a730b230b39f2ba8d6f253263b5c3948b1a3d8a3467f" +
"7cfcdd1fdd6bff7828fda12784fd277be8c680fcdf2cc4676acff5df759f" +
"f4bc712ee1a560157233cbf6bb4bcb91dd1c5d2824b42f4913e4715c1ba4" +
"001fde0d90c274bfa81a79e4a0d00a7ddcbfdd8de4183b497487a20d0203" +
"010001a000300d06092a864886f70d01010b050003820101000ead204cfd" +
"45d307dd49de6937d7e2d8abf17490a49a8cee5250ef7799ef53229f8cfc" +
"735b9f65d789f898945f3d5536a09932e241050bd5473c47a4ac2493707f" +
"1142bf9a06d047384ad463463acb3744d435b4cff8c8b0f9673e8700e13b" +
"6bc99a486823fa85f7707e1bb8430e62541715ab6cb3fae3efb8356042a5" +
"c9f493dd08eff690570cce65cffc4fe354aa40957dc16a37a833aa968f62" +
"693d5059d53f6a96a159195d3fb7b558d462de63d945d4e3680d2b1f2c98" +
"33c3bfd92a9235de3d345a431ee5a675e0e18308bd2729413acd84432da4" +
"2410e1b87ae70227dd9a98e49ee6aeea9eaff67f968691918201e94697f2" +
"da010d6f939cea40c26038"
// CSR generated by Go:
// * Random public key
// * C = US
// * CN = [none]
// * DNSNames = not-example.com
var NO_CN_CSR_HEX = "3082027f30820167020100300d310b300906035504061302555330820122" +
"300d06092a864886f70d01010105000382010f003082010a0282010100d8" +
"b3c11610ce17614f6d78de3f079db430e479c38978da8cd625b7c70dd445" +
"57fd99b9831693e6b9b09fb7c74a82058a1f1a4e1e087f04f93aa73bc35a" +
"688440205a6f5fd56ff478c5554b14c3b2a1a0b5eed1aef7189ad848e117" +
"04b1eb6c29b47ada40a5719a38ce2f2869896bf5405c2bafd4c7dfb99c0e" +
"9f26f80145e16b73bbacf67aedcd3b7ce57bb5b67cf692aec7956d23c236" +
"2336c2408b65469630dccca3ca006f28e36ca8c95dda84b6586f29c8de63" +
"661c09b58253e386a74707394cbba4de165f2745a65b717b9fd4b8b84c09" +
"85583b5c17d3e88bbf71c88eeeccb5d552d61cde7835ec83d6ec9b41114a" +
"0583f8eeae8a536cb3ca5786c22ab30203010001a02d302b06092a864886" +
"f70d01090e311e301c301a0603551d1104133011820f6e6f742d6578616d" +
"706c652e636f6d300d06092a864886f70d01010b05000382010100430239" +
"8db6b64b94d93399db32335232967ca6a748048483db8cb2b387871f758c" +
"6f7bf1593624b142127847cd2a511897bbadd8ad038468fb309fa2161031" +
"949b9ba24931b0d363ad2f8dae56a4c908ba748d41c664aa129dcb1a6f88" +
"0b90502cd244d9abd8dd5e78f763730660655a350f1c25af95cf1f89dda9" +
"076f4e6b84b6da9a98ed87f538624e4338fa0ff1a404e763dd6800694a21" +
"d28595927606308aefa1ac7e8f5600b05e33c0a7b25d3a9f5032c7c25264" +
"026c039733b179315254af4f25e90a1d00facd69313b36fdc66a5818fb49" +
"a0d90e0745d66a82d337289c9968b3ec4a4826c530c758cacecc18e06366" +
"dd8962c451c3ce22c2aed33726"
// CSR generated by Go:
// * Random public key
// * C = US
// * CN = [none]
// * DNSNames = [none]
var NO_NAME_CSR_HEX = "308202523082013a020100300d310b300906035504061302555330820122" +
"300d06092a864886f70d01010105000382010f003082010a0282010100bc" +
"fae49f68f02c42500b2faf251628ee19e8ef048a35fef311c9c419c80606" +
"ab37340ad6e25cf4cc63c0283994b4ba705d86950ad5298094e0b9684647" +
"8d67abc695741317b4ff8da9fd33120342559cfdaf9109ac403f0d0bf9ff" +
"54dd79fa2256b218a9bdb17c608167c7fcad4cf839733c7eab9589fe6137" +
"e99bb24c24b7eb74e19f51ffee4ea62c4ab756f099ff5197c5032f60edff" +
"36022b8a99d35aeb706854fa9a31ea8a362a2251f08b93023b32e1df771a" +
"970f08a30ced656950b8ef71600d65d6995a0b92903b179c05a76f702a08" +
"0b41402c308d8ab57f14b5516b89fe317e38e13d7adad7f7025743610881" +
"9fb60268f0773b08b62ac8c8c84f2d0203010001a000300d06092a864886" +
"f70d01010b050003820101001eda9ce8253e8b933348851acd38ab63cd64" +
"f833d7ffc711f1b6e6a37a7deb7ad44b5589d90533ed61dfd48cab2775e2" +
"a19c41f5cb69faa9dde856606822a3bf798381836214154c17bc037f23ad" +
"67c84d876855c0aea871dc55bd14b2cd267e49b734bc7a38c29c334611bf" +
"ec7efdc56a1512e25fd12ca99a5809b1b6a808caf6a8baefff7fb2bda454" +
"5c226849674900ce7a1f90287ab31be80a4e2b6d64765b9d973628e60299" +
"6423edd74e7a58005bd520d4173f0c30d935de530477480d7725d9758f9a" +
"58c004d9e1e55af59ea517dfbd2bccca58216d8130b9f77c90328b2aa54b" +
"1778a629b584f2bc059489a236131de9b444adca90218c31a499a485"
func TestIssueCertificate(t *testing.T) {
// Decode pre-generated values
@ -135,28 +226,70 @@ func TestIssueCertificate(t *testing.T) {
caCertPEM, _ := pem.Decode([]byte(CA_CERT_PEM))
caCert, _ := x509.ParseCertificate(caCertPEM.Bytes)
csrDER, _ := hex.DecodeString(CSR_HEX)
csr, _ := x509.ParseCertificateRequest(csrDER)
// Uncomment to create a CFSSL local signer
// Create a CFSSL local signer
signer, _ := local.NewSigner(caKey, caCert, x509.SHA256WithRSA, nil)
// CFSSL config
hostPort := "localhost:9000"
authKey := "79999d86250c367a2b517a1ae7d409c1"
profileName := "ee"
// Create an SA
sa, err := sa.NewSQLStorageAuthority("sqlite3", ":memory:")
test.AssertNotError(t, err, "Failed to create SA")
sa.InitTables()
// Create an online CFSSL instance
// This is designed to mimic what LE plans to do
authHandler, err := auth.New(authKey, nil)
test.AssertNotError(t, err, "Failed to create authentication handler")
policy := &config.Signing{
Profiles: map[string]*config.SigningProfile{
profileName: &config.SigningProfile{
Usage: []string{"server auth"},
CA: false,
IssuerURL: []string{"http://not-example.com/issuer-url"},
OCSP: "http://not-example.com/ocsp",
CRL: "http://not-example.com/crl",
Policies: []asn1.ObjectIdentifier{
asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1},
},
Expiry: 8760 * time.Hour,
Backdate: time.Hour,
Provider: authHandler,
},
},
Default: &config.SigningProfile{
Expiry: time.Hour,
},
}
signer, err := local.NewSigner(caKey, caCert, x509.SHA256WithRSA, policy)
test.AssertNotError(t, err, "Failed to create signer")
signHandler, err := apisign.NewAuthHandlerFromSigner(signer)
test.AssertNotError(t, err, "Failed to create signing API endpoint")
http.Handle("/api/v1/cfssl/authsign", signHandler)
// This goroutine should get killed when main() return
go (func() { http.ListenAndServe(hostPort, nil) })()
// Create a CA
/*
// Uncomment to test with a remote signer
ca, err := NewCertificateAuthorityImpl("localhost:9000", "79999d86250c367a2b517a1ae7d409c1", "ee")
ca, err := NewCertificateAuthorityImpl(hostPort, authKey, profileName)
test.AssertNotError(t, err, "Failed to create CA")
ca.SA = sa
*/
/*
// Uncomment to test with a local signer
signer, _ := local.NewSigner(caKey, caCert, x509.SHA256WithRSA, nil)
ca := CertificateAuthorityImpl{
Signer: signer,
SA: sa,
}
*/
csrs := []string{CN_AND_SAN_CSR_HEX, NO_SAN_CSR_HEX, NO_CN_CSR_HEX}
for _, csrHEX := range csrs {
csrDER, _ := hex.DecodeString(csrHEX)
csr, _ := x509.ParseCertificateRequest(csrDER)
// Sign CSR
certObj, err := ca.IssueCertificate(*csr)
@ -166,13 +299,27 @@ func TestIssueCertificate(t *testing.T) {
cert, err := x509.ParseCertificate(certObj.DER)
test.AssertNotError(t, err, "Certificate failed to parse")
test.AssertEquals(t, cert.Subject.CommonName, "example.com")
test.AssertEquals(t, cert.Subject.CommonName, "not-example.com")
if len(cert.DNSNames) != 2 || cert.DNSNames[0] != "example.com" || cert.DNSNames[1] != "www.example.com" {
if len(cert.DNSNames) == 0 || cert.DNSNames[0] != "not-example.com" {
// NB: This does not check for www.not-example.com in the 'both' case
t.Errorf("Improper list of domain names %v", cert.DNSNames)
}
if len(cert.Subject.Country) > 0 {
t.Errorf("Subject contained unauthorized values")
}
// Verify that the cert got stored in the DB
_, err = sa.GetCertificate(certObj.ID)
test.AssertNotError(t, err, "Certificate not found in database")
}
// Test that the CA rejects CSRs with no names
csrDER, _ := hex.DecodeString(NO_NAME_CSR_HEX)
csr, _ := x509.ParseCertificateRequest(csrDER)
_, err = ca.IssueCertificate(*csr)
if err == nil {
t.Errorf("CA improperly agreed to create a certificate with no name")
}
}

View File

@ -55,7 +55,6 @@ func main() {
app.Usage = "Command-line utility to start Boulder's servers in stand-alone mode"
app.Version = "0.0.0"
// Specify AMQP Server
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "amqp",
@ -97,12 +96,6 @@ func main() {
Usage: "Start the CA in monolithic mode, without using AMQP",
Action: func(c *cli.Context) {
// XXX Print the config
fmt.Println(c.GlobalString("amqp"))
fmt.Println(c.GlobalString("cfssl"))
fmt.Println(c.GlobalString("cfsslAuthKey"))
fmt.Println(c.GlobalString("cfsslProfile"))
// Grab parameters
cfsslServer := c.GlobalString("cfssl")
authKey := c.GlobalString("cfsslAuthKey")
@ -112,6 +105,8 @@ func main() {
wfe := wfe.NewWebFrontEndImpl()
sa, err := sa.NewSQLStorageAuthority("sqlite3", ":memory:")
failOnError(err, "Unable to create SA")
err = sa.InitTables()
failOnError(err, "Unable to initialize SA")
ra := ra.NewRegistrationAuthorityImpl()
va := va.NewValidationAuthorityImpl()
ca, err := ca.NewCertificateAuthorityImpl(cfsslServer, authKey, profile)
@ -124,17 +119,40 @@ func main() {
ra.SA = sa
ra.VA = &va
va.RA = &ra
ca.SA = sa
// Go!
authority := "0.0.0.0:4000"
urlBase := "http://" + authority
newRegPath := "/acme/new-reg"
regPath := "/acme/reg/"
newAuthzPath := "/acme/new-authz"
authzPath := "/acme/authz/"
newCertPath := "/acme/new-cert"
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)
wfe.NewReg = urlBase + newRegPath
wfe.RegBase = urlBase + regPath
wfe.NewAuthz = urlBase + newAuthzPath
wfe.AuthzBase = urlBase + authzPath
wfe.NewCert = urlBase + newCertPath
wfe.CertBase = urlBase + certPath
http.HandleFunc(newRegPath, wfe.NewRegistration)
http.HandleFunc(newAuthzPath, wfe.NewAuthorization)
http.HandleFunc(newCertPath, wfe.NewCertificate)
http.HandleFunc(regPath, wfe.Registration)
http.HandleFunc(authzPath, wfe.Authorization)
http.HandleFunc(certPath, wfe.Certificate)
// Add a simple ToS
termsPath := "/terms"
http.HandleFunc(termsPath, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "You agree to do the right thing")
})
wfe.SubscriberAgreementURL = urlBase + termsPath
// We need to tell the RA how to make challenge URIs
// XXX: Better way to do this? Part of improved configuration
ra.AuthzBase = wfe.AuthzBase
fmt.Fprintf(os.Stderr, "Server running...\n")
err = http.ListenAndServe(authority, nil)
@ -201,14 +219,25 @@ func main() {
// Go!
authority := "0.0.0.0:4000"
urlBase := "http://" + authority
newRegPath := "/acme/new-reg"
regPath := "/acme/reg/"
newAuthzPath := "/acme/new-authz"
authzPath := "/acme/authz/"
newCertPath := "/acme/new-cert"
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)
wfe.NewReg = urlBase + newRegPath
wfe.RegBase = urlBase + regPath
wfe.NewAuthz = urlBase + newAuthzPath
wfe.AuthzBase = urlBase + authzPath
wfe.NewCert = urlBase + newCertPath
wfe.CertBase = urlBase + certPath
http.HandleFunc(newRegPath, wfe.NewRegistration)
http.HandleFunc(newAuthzPath, wfe.NewAuthorization)
http.HandleFunc(newCertPath, wfe.NewCertificate)
http.HandleFunc(regPath, wfe.Registration)
http.HandleFunc(authzPath, wfe.Authorization)
http.HandleFunc(certPath, wfe.Certificate)
fmt.Fprintf(os.Stderr, "Server running...\n")
err = http.ListenAndServe(authority, nil)
@ -235,14 +264,25 @@ func main() {
// Connect the front end to HTTP
authority := "0.0.0.0:4000"
urlBase := "http://" + authority
newRegPath := "/acme/new-reg"
regPath := "/acme/reg/"
newAuthzPath := "/acme/new-authz"
authzPath := "/acme/authz/"
newCertPath := "/acme/new-cert"
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)
wfe.NewReg = urlBase + newRegPath
wfe.RegBase = urlBase + regPath
wfe.NewAuthz = urlBase + newAuthzPath
wfe.AuthzBase = urlBase + authzPath
wfe.NewCert = urlBase + newCertPath
wfe.CertBase = urlBase + certPath
http.HandleFunc(newRegPath, wfe.NewRegistration)
http.HandleFunc(newAuthzPath, wfe.NewAuthorization)
http.HandleFunc(newCertPath, wfe.NewCertificate)
http.HandleFunc(regPath, wfe.Registration)
http.HandleFunc(authzPath, wfe.Authorization)
http.HandleFunc(certPath, wfe.Certificate)
fmt.Fprintf(os.Stderr, "Server running...\n")
http.ListenAndServe(authority, nil)

View File

@ -12,6 +12,7 @@ import (
func SimpleHTTPSChallenge() Challenge {
return Challenge{
Type: ChallengeTypeSimpleHTTPS,
Status: StatusPending,
Token: NewToken(),
}
@ -21,6 +22,7 @@ func DvsniChallenge() Challenge {
nonce := make([]byte, 16)
rand.Read(nonce)
return Challenge{
Type: ChallengeTypeDVSNI,
Status: StatusPending,
R: RandomString(32),
Nonce: hex.EncodeToString(nonce),

View File

@ -246,6 +246,9 @@ func TestURL(t *testing.T) {
url.URL.Path != path || url.URL.RawQuery != query {
t.Errorf("Improper URL contents: %v", url.URL)
}
if s := url.URL.PathSegments(); len(s) != 2 {
t.Errorf("Path segments failed to parse properly: %v", s)
}
err = json.Unmarshal([]byte(badJSON), &url)
if err == nil {

View File

@ -29,12 +29,18 @@ type WebFrontEnd interface {
// Set the base URL for certificates
SetCertBase(path string)
// This method represents the ACME new-registration resource
NewRegistration(response http.ResponseWriter, request *http.Request)
// 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 registration resources
Registration(response http.ResponseWriter, request *http.Request)
// Provide access to requests for authorization resources
Authz(response http.ResponseWriter, request *http.Request)
@ -43,6 +49,9 @@ type WebFrontEnd interface {
}
type RegistrationAuthority interface {
// [WebFrontEnd]
NewRegistration(Registration, jose.JsonWebKey) (Registration, error)
// [WebFrontEnd]
NewAuthorization(Authorization, jose.JsonWebKey) (Authorization, error)
@ -50,7 +59,10 @@ type RegistrationAuthority interface {
NewCertificate(CertificateRequest, jose.JsonWebKey) (Certificate, error)
// [WebFrontEnd]
UpdateAuthorization(Authorization) (Authorization, error)
UpdateRegistration(Registration, Registration) (Registration, error)
// [WebFrontEnd]
UpdateAuthorization(Authorization, int, Challenge) (Authorization, error)
// [WebFrontEnd]
RevokeCertificate(x509.Certificate) error
@ -70,15 +82,20 @@ type CertificateAuthority interface {
}
type StorageGetter interface {
GetCertificate(string) ([]byte, error)
GetRegistration(string) (Registration, error)
GetAuthorization(string) (Authorization, error)
GetCertificate(string) ([]byte, error)
}
type StorageAdder interface {
AddCertificate([]byte) (string, error)
NewRegistration() (string, error)
UpdateRegistration(Registration) error
NewPendingAuthorization() (string, error)
UpdatePendingAuthorization(Authorization) error
FinalizeAuthorization(Authorization) error
AddCertificate([]byte) (string, error)
}
// The StorageAuthority interface represnts a simple key/value

View File

@ -88,10 +88,43 @@ func (cr CertificateRequest) MarshalJSON() ([]byte, error) {
})
}
// Registration objects represent non-public metadata attached
// to account keys.
type Registration struct {
// Unique identifier
ID string `json:"-"`
// Account key to which the details are attached
Key jose.JsonWebKey `json:"key"`
// Recovery Token is used to prove connection to an earlier transaction
RecoveryToken string `json:"recoveryToken"`
// Contact URIs
Contact []AcmeURL `json:"contact,omitempty"`
// Agreement with terms of service
Agreement string `json:"agreement,omitempty"`
}
func (r *Registration) MergeUpdate(input Registration) {
if len(input.Contact) > 0 {
r.Contact = input.Contact
}
// TODO: Test to make sure this has the proper value
if len(input.Agreement) > 0 {
r.Agreement = input.Agreement
}
}
// 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 type of challenge
Type string `json:"type"`
// The status of this challenge
Status AcmeStatus `json:"status,omitempty"`
@ -99,6 +132,9 @@ type Challenge struct {
// was completed by the server.
Completed time.Time `json:"completed,omitempty"`
// A URI to which a response can be POSTed
URI AcmeURL `json:"uri"`
// Used by simpleHttps, recoveryToken, and dns challenges
Token string `json:"token,omitempty"`
@ -112,6 +148,7 @@ type Challenge struct {
}
// Merge a client-provide response to a challenge with the issued challenge
// TODO: Remove return type from this method
func (ch Challenge) MergeResponse(resp Challenge) Challenge {
// Only override fields that are supposed to be client-provided
if len(ch.Path) == 0 {
@ -153,11 +190,11 @@ type Authorization struct {
// 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"`
Challenges []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"`
Combinations [][]int `json:"combinations,omitempty"`
// The client may provide contact URIs to allow the server
// to push information to it.

View File

@ -89,6 +89,14 @@ func Fingerprint256(data []byte) string {
// URLs that automatically marshal/unmarshal to JSON strings
type AcmeURL url.URL
func (u AcmeURL) PathSegments() (segments []string) {
segments = strings.Split(u.Path, "/")
if len(segments) > 0 && len(segments[0]) == 0 {
segments = segments[1:]
}
return
}
func (u AcmeURL) MarshalJSON() ([]byte, error) {
uu := url.URL(u)
return json.Marshal(uu.String())

View File

@ -8,6 +8,7 @@ package ra
import (
"crypto/x509"
"fmt"
"net/url"
"regexp"
"strings"
"time"
@ -22,6 +23,8 @@ type RegistrationAuthorityImpl struct {
CA core.CertificateAuthority
VA core.ValidationAuthority
SA core.StorageAuthority
AuthzBase string
}
func NewRegistrationAuthorityImpl() RegistrationAuthorityImpl {
@ -83,6 +86,24 @@ func lastPathSegment(url core.AcmeURL) string {
return allButLastPathSegment.ReplaceAllString(url.Path, "")
}
func (ra *RegistrationAuthorityImpl) NewRegistration(init core.Registration, key jose.JsonWebKey) (reg core.Registration, err error) {
regID, err := ra.SA.NewRegistration()
if err != nil {
return
}
reg = core.Registration{
ID: regID,
Key: key,
RecoveryToken: core.NewToken(),
}
reg.MergeUpdate(init)
// Store the authorization object, then return it
err = ra.SA.UpdateRegistration(reg)
return
}
func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization, key jose.JsonWebKey) (authz core.Authorization, err error) {
identifier := request.Identifier
@ -99,9 +120,18 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization
}
// Create validations
// TODO: Assign URLs
simpleHttps := core.SimpleHTTPSChallenge()
dvsni := core.DvsniChallenge()
authID, err := ra.SA.NewPendingAuthorization()
if err != nil {
return
}
// Ignoring these errors because we construct the URLs to be correct
simpleHTTPSURI, _ := url.Parse(ra.AuthzBase + authID + "?" + core.RandomString(4))
dvsniURI, _ := url.Parse(ra.AuthzBase + authID + "?" + core.RandomString(4))
simpleHttps.URI = core.AcmeURL(*simpleHTTPSURI)
dvsni.URI = core.AcmeURL(*dvsniURI)
// Create a new authorization object
authz = core.Authorization{
@ -109,9 +139,9 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization
Identifier: identifier,
Key: key,
Status: core.StatusPending,
Challenges: map[string]core.Challenge{
core.ChallengeTypeSimpleHTTPS: simpleHttps,
core.ChallengeTypeDVSNI: dvsni,
Challenges: []core.Challenge{
simpleHttps,
dvsni,
},
}
@ -167,37 +197,29 @@ func (ra *RegistrationAuthorityImpl) NewCertificate(req core.CertificateRequest,
return
}
func (ra *RegistrationAuthorityImpl) UpdateAuthorization(delta core.Authorization) (authz core.Authorization, err error) {
// Fetch the copy of this authorization we have on file
authz, err = ra.SA.GetAuthorization(delta.ID)
if err != nil {
func (ra *RegistrationAuthorityImpl) UpdateRegistration(base core.Registration, update core.Registration) (reg core.Registration, err error) {
base.MergeUpdate(update)
reg = base
err = ra.SA.UpdateRegistration(base)
return
}
func (ra *RegistrationAuthorityImpl) UpdateAuthorization(base core.Authorization, challengeIndex int, response core.Challenge) (authz core.Authorization, err error) {
// Copy information over that the client is allowed to supply
authz = base
if challengeIndex >= len(authz.Challenges) {
err = core.MalformedRequestError("Invalid challenge index")
return
}
// 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)
}
authz.Challenges[challengeIndex] = authz.Challenges[challengeIndex].MergeResponse(response)
// Store the updated version
ra.SA.UpdatePendingAuthorization(authz)
// If any challenges were updated, dispatch to the VA for service
if newResponse {
// Dispatch to the VA for service
ra.VA.UpdateValidations(authz)
}
return authz, nil
return
}
func (ra *RegistrationAuthorityImpl) RevokeCertificate(cert x509.Certificate) error {

View File

@ -120,15 +120,9 @@ var (
},
}
AuthzDelta = core.Authorization{
Challenges: map[string]core.Challenge{
core.ChallengeTypeSimpleHTTPS: core.Challenge{
ResponseIndex = 0
Response = core.Challenge{
Path: "Hf5GrX4Q7EBax9hc2jJnfw",
},
core.ChallengeTypeDVSNI: core.Challenge{
S: "23029d88d9e123e",
},
},
}
ExampleCSR = &x509.CertificateRequest{}
@ -193,10 +187,7 @@ func TestNewAuthorization(t *testing.T) {
test.Assert(t, authz.Identifier == AuthzRequest.Identifier, "Initial authz had wrong identifier")
test.Assert(t, authz.Status == core.StatusPending, "Initial authz not pending")
_, ok := authz.Challenges[core.ChallengeTypeDVSNI]
test.Assert(t, ok, "Initial authz does not include DVSNI challenge")
_, ok = authz.Challenges[core.ChallengeTypeSimpleHTTPS]
test.Assert(t, ok, "Initial authz does not include SimpleHTTPS challenge")
// TODO Verify challenges
// If we get to here, we'll use this authorization for the next test
AuthzInitial = authz
@ -209,9 +200,8 @@ func TestUpdateAuthorization(t *testing.T) {
_, va, sa, ra := initAuthorities(t)
AuthzInitial.ID, _ = sa.NewPendingAuthorization()
sa.UpdatePendingAuthorization(AuthzInitial)
AuthzDelta.ID = AuthzInitial.ID
authz, err := ra.UpdateAuthorization(AuthzDelta)
authz, err := ra.UpdateAuthorization(AuthzInitial, ResponseIndex, Response)
test.AssertNotError(t, err, "UpdateAuthorization failed")
// Verify that returned authz same as DB
@ -224,14 +214,9 @@ func TestUpdateAuthorization(t *testing.T) {
assertAuthzEqual(t, authz, va.Argument)
// Verify that the responses are reflected
simpleHttps, ok := va.Argument.Challenges[core.ChallengeTypeSimpleHTTPS]
simpleHttpsOrig, _ := AuthzDelta.Challenges[core.ChallengeTypeSimpleHTTPS]
test.Assert(t, ok, "Authz passed to VA has no simpleHttps challenge")
test.Assert(t, simpleHttps.Path == simpleHttpsOrig.Path, "simpleHttps changed")
dvsni, ok := va.Argument.Challenges[core.ChallengeTypeDVSNI]
dvsniOrig, _ := AuthzDelta.Challenges[core.ChallengeTypeDVSNI]
test.Assert(t, ok, "Authz passed to VA has no dvsni challenge")
test.Assert(t, dvsni.Token == dvsniOrig.Token, "dvsni changed")
test.Assert(t, len(va.Argument.Challenges) > 0, "Authz passed to VA has no challenges")
simpleHttps := va.Argument.Challenges[0]
test.Assert(t, simpleHttps.Path == Response.Path, "simpleHttps changed")
// If we get to here, we'll use this authorization for the next test
AuthzUpdated = authz
@ -247,9 +232,7 @@ func TestOnValidationUpdate(t *testing.T) {
// Simulate a successful simpleHttps challenge
AuthzFromVA = AuthzUpdated
challenge := AuthzFromVA.Challenges[core.ChallengeTypeSimpleHTTPS]
challenge.Status = core.StatusValid
AuthzFromVA.Challenges[core.ChallengeTypeSimpleHTTPS] = challenge
AuthzFromVA.Challenges[0].Status = core.StatusValid
ra.OnValidationUpdate(AuthzFromVA)

View File

@ -33,19 +33,22 @@ import (
// so it doesn't need wrappers.
const (
MethodNewRegistration = "NewRegistration" // RA, SA
MethodNewAuthorization = "NewAuthorization" // RA
MethodNewCertificate = "NewCertificate" // RA
MethodUpdateRegistration = "UpdateRegistration" // RA, SA
MethodUpdateAuthorization = "UpdateAuthorization" // RA
MethodRevokeCertificate = "RevokeCertificate" // RA
MethodOnValidationUpdate = "OnValidationUpdate" // RA
MethodUpdateValidations = "UpdateValidations" // VA
MethodIssueCertificate = "IssueCertificate" // CA
MethodGetCertificate = "GetCertificate" // SA
MethodGetRegistration = "GetRegistration" // SA
MethodGetAuthorization = "GetAuthorization" // SA
MethodAddCertificate = "AddCertificate" // SA
MethodGetCertificate = "GetCertificate" // SA
MethodNewPendingAuthorization = "NewPendingAuthorization" // SA
MethodUpdatePendingAuthorization = "UpdatePendingAuthorization" // SA
MethodFinalizeAuthorization = "FinalizeAuthorization" // SA
MethodAddCertificate = "AddCertificate" // SA
)
// RegistrationAuthorityClient / Server
@ -54,6 +57,11 @@ const (
// -> UpdateAuthorization
// -> RevokeCertificate
// -> OnValidationUpdate
type registrationRequest struct {
Reg core.Registration
Key jose.JsonWebKey
}
type authorizationRequest struct {
Authz core.Authorization
Key jose.JsonWebKey
@ -67,6 +75,25 @@ type certificateRequest struct {
func NewRegistrationAuthorityServer(serverQueue string, channel *amqp.Channel, impl core.RegistrationAuthority) (rpc *AmqpRpcServer, err error) {
rpc = NewAmqpRpcServer(serverQueue, channel)
rpc.Handle(MethodNewRegistration, func(req []byte) (response []byte) {
var rr registrationRequest
err := json.Unmarshal(req, &rr)
if err != nil {
return
}
reg, err := impl.NewRegistration(rr.Reg, rr.Key)
if err != nil {
return
}
response, err = json.Marshal(reg)
if err != nil {
response = []byte{}
}
return
})
rpc.Handle(MethodNewAuthorization, func(req []byte) (response []byte) {
var ar authorizationRequest
err := json.Unmarshal(req, &ar)
@ -111,14 +138,39 @@ func NewRegistrationAuthorityServer(serverQueue string, channel *amqp.Channel, i
return
})
rpc.Handle(MethodUpdateRegistration, func(req []byte) (response []byte) {
var request struct {
Base, Update core.Registration
}
err := json.Unmarshal(req, &request)
if err != nil {
return
}
reg, err := impl.UpdateRegistration(request.Base, request.Update)
if err != nil {
return
}
response, err = json.Marshal(reg)
if err != nil {
response = []byte{}
}
return
})
rpc.Handle(MethodUpdateAuthorization, func(req []byte) (response []byte) {
var authz core.Authorization
var authz struct {
Authz core.Authorization
Index int
Response core.Challenge
}
err := json.Unmarshal(req, &authz)
if err != nil {
return
}
newAuthz, err := impl.UpdateAuthorization(authz)
newAuthz, err := impl.UpdateAuthorization(authz.Authz, authz.Index, authz.Response)
if err != nil {
return
}
@ -174,6 +226,21 @@ func NewRegistrationAuthorityClient(clientQueue, serverQueue string, channel *am
return
}
func (rac RegistrationAuthorityClient) NewRegistration(reg core.Registration, key jose.JsonWebKey) (newReg core.Registration, err error) {
data, err := json.Marshal(registrationRequest{reg, key})
if err != nil {
return
}
newRegData, err := rac.rpc.DispatchSync(MethodNewRegistration, data)
if err != nil || len(newRegData) == 0 {
return
}
err = json.Unmarshal(newRegData, &newReg)
return
}
func (rac RegistrationAuthorityClient) NewAuthorization(authz core.Authorization, key jose.JsonWebKey) (newAuthz core.Authorization, err error) {
data, err := json.Marshal(authorizationRequest{authz, key})
if err != nil {
@ -204,8 +271,36 @@ func (rac RegistrationAuthorityClient) NewCertificate(cr core.CertificateRequest
return
}
func (rac RegistrationAuthorityClient) UpdateAuthorization(authz core.Authorization) (newAuthz core.Authorization, err error) {
data, err := json.Marshal(authz)
func (rac RegistrationAuthorityClient) UpdateRegistration(base core.Registration, update core.Registration) (newReg core.Registration, err error) {
var toSend struct{ Base, Update core.Registration }
toSend.Base = base
toSend.Update = update
data, err := json.Marshal(toSend)
if err != nil {
return
}
newRegData, err := rac.rpc.DispatchSync(MethodUpdateRegistration, data)
if err != nil || len(newRegData) == 0 {
return
}
err = json.Unmarshal(newRegData, &newReg)
return
}
func (rac RegistrationAuthorityClient) UpdateAuthorization(authz core.Authorization, index int, response core.Challenge) (newAuthz core.Authorization, err error) {
var toSend struct {
Authz core.Authorization
Index int
Response core.Challenge
}
toSend.Authz = authz
toSend.Index = index
toSend.Response = response
data, err := json.Marshal(toSend)
if err != nil {
return
}
@ -337,11 +432,17 @@ func (cac CertificateAuthorityClient) IssueCertificate(csr x509.CertificateReque
func NewStorageAuthorityServer(serverQueue string, channel *amqp.Channel, impl core.StorageAuthority) (rpc *AmqpRpcServer) {
rpc = NewAmqpRpcServer(serverQueue, channel)
rpc.Handle(MethodGetCertificate, func(req []byte) (response []byte) {
cert, err := impl.GetCertificate(string(req))
if err == nil {
response = []byte(cert)
rpc.Handle(MethodGetRegistration, func(req []byte) (response []byte) {
reg, err := impl.GetCertificate(string(req))
if err != nil {
return
}
jsonReg, err := json.Marshal(reg)
if err != nil {
return
}
response = jsonReg
return
})
@ -366,6 +467,14 @@ func NewStorageAuthorityServer(serverQueue string, channel *amqp.Channel, impl c
return
})
rpc.Handle(MethodNewRegistration, func(req []byte) (response []byte) {
id, err := impl.NewRegistration()
if err == nil {
response = []byte(id)
}
return
})
rpc.Handle(MethodNewPendingAuthorization, func(req []byte) (response []byte) {
id, err := impl.NewPendingAuthorization()
if err == nil {
@ -385,17 +494,6 @@ func NewStorageAuthorityServer(serverQueue string, channel *amqp.Channel, impl c
return
})
rpc.Handle(MethodUpdatePendingAuthorization, func(req []byte) (response []byte) {
var authz core.Authorization
err := json.Unmarshal(req, authz)
if err != nil {
return
}
impl.UpdatePendingAuthorization(authz)
return
})
rpc.Handle(MethodFinalizeAuthorization, func(req []byte) (response []byte) {
var authz core.Authorization
err := json.Unmarshal(req, authz)
@ -407,6 +505,14 @@ func NewStorageAuthorityServer(serverQueue string, channel *amqp.Channel, impl c
return
})
rpc.Handle(MethodGetCertificate, func(req []byte) (response []byte) {
cert, err := impl.GetCertificate(string(req))
if err == nil {
response = []byte(cert)
}
return
})
return
}
@ -424,8 +530,13 @@ func NewStorageAuthorityClient(clientQueue, serverQueue string, channel *amqp.Ch
return
}
func (cac StorageAuthorityClient) GetCertificate(id string) (cert []byte, err error) {
cert, err = cac.rpc.DispatchSync(MethodGetCertificate, []byte(id))
func (cac StorageAuthorityClient) GetRegistration(id string) (reg core.Registration, err error) {
jsonReg, err := cac.rpc.DispatchSync(MethodGetRegistration, []byte(id))
if err != nil {
return
}
err = json.Unmarshal(jsonReg, &reg)
return
}
@ -439,10 +550,26 @@ func (cac StorageAuthorityClient) GetAuthorization(id string) (authz core.Author
return
}
func (cac StorageAuthorityClient) AddCertificate(cert []byte) (id string, err error) {
response, err := cac.rpc.DispatchSync(MethodAddCertificate, cert)
func (cac StorageAuthorityClient) GetCertificate(id string) (cert []byte, err error) {
cert, err = cac.rpc.DispatchSync(MethodGetCertificate, []byte(id))
return
}
func (cac StorageAuthorityClient) UpdateRegistration(reg core.Registration) (err error) {
jsonReg, err := json.Marshal(reg)
if err != nil {
return
}
// XXX: Is this catching all the errors?
_, err = cac.rpc.DispatchSync(MethodUpdatePendingAuthorization, jsonReg)
return
}
func (cac StorageAuthorityClient) NewRegistration() (id string, err error) {
response, err := cac.rpc.DispatchSync(MethodNewPendingAuthorization, []byte{})
if err != nil || len(response) == 0 {
err = errors.New("AddCertificate RPC failed") // XXX
err = errors.New("NewRegistration RPC failed") // XXX
return
}
id = string(response)
@ -452,7 +579,7 @@ func (cac StorageAuthorityClient) AddCertificate(cert []byte) (id string, err er
func (cac StorageAuthorityClient) NewPendingAuthorization() (id string, err error) {
response, err := cac.rpc.DispatchSync(MethodNewPendingAuthorization, []byte{})
if err != nil || len(response) == 0 {
err = errors.New("AddCertificate RPC failed") // XXX
err = errors.New("NewPendingAuthorization RPC failed") // XXX
return
}
id = string(response)
@ -480,3 +607,13 @@ func (cac StorageAuthorityClient) FinalizeAuthorization(authz core.Authorization
_, err = cac.rpc.DispatchSync(MethodFinalizeAuthorization, jsonAuthz)
return
}
func (cac StorageAuthorityClient) AddCertificate(cert []byte) (id string, err error) {
response, err := cac.rpc.DispatchSync(MethodAddCertificate, cert)
if err != nil || len(response) == 0 {
err = errors.New("AddCertificate RPC failed") // XXX
return
}
id = string(response)
return
}

View File

@ -8,8 +8,10 @@ package sa
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/letsencrypt/boulder/core"
)
@ -47,8 +49,8 @@ func (ssa *SQLStorageAuthority) InitTables() (err error) {
return
}
// Create certificates table
_, err = tx.Exec("CREATE TABLE certificates (sequence INTEGER, digest TEXT, value BLOB);")
// Create registrations table
_, err = tx.Exec("CREATE TABLE registrations (id TEXT, thumbprint TEXT, value TEXT);")
if err != nil {
tx.Rollback()
return
@ -68,13 +70,38 @@ func (ssa *SQLStorageAuthority) InitTables() (err error) {
return
}
// Create certificates table
_, err = tx.Exec("CREATE TABLE certificates (sequence INTEGER, digest TEXT, value BLOB);")
if err != nil {
tx.Rollback()
return
}
err = tx.Commit()
return
}
func (ssa *SQLStorageAuthority) GetCertificate(id string) (cert []byte, err error) {
err = ssa.db.QueryRow("SELECT value FROM certificates WHERE digest = ?;", id).Scan(&cert)
return
func (ssa *SQLStorageAuthority) dumpTables(tx *sql.Tx) {
fmt.Printf("===== TABLE DUMP =====\n")
fmt.Printf("\n----- registrations -----\n")
rows, err := tx.Query("SELECT id, thumbprint, value FROM registrations")
if err != nil {
fmt.Printf("ERROR: %v\n", err)
} else {
defer rows.Close()
for rows.Next() {
var id, key, value []byte
if err := rows.Scan(&id, &key, &value); err == nil {
fmt.Printf("%s | %s | %s\n", string(id), string(key), hex.EncodeToString(value))
} else {
fmt.Printf("ERROR: %v\n", err)
}
}
}
fmt.Printf("\n----- pending_authz -----\n") // TODO
fmt.Printf("\n----- authz -----\n") // TODO
fmt.Printf("\n----- certificates -----\n") // TODO
}
func statusIsPending(status core.AcmeStatus) bool {
@ -91,6 +118,22 @@ func existingFinal(tx *sql.Tx, id string) (count int64) {
return
}
func existingRegistration(tx *sql.Tx, id string) (count int64) {
tx.QueryRow("SELECT count(*) FROM registrations WHERE id = ?;", id).Scan(&count)
return
}
func (ssa *SQLStorageAuthority) GetRegistration(id string) (reg core.Registration, err error) {
var jsonReg []byte
err = ssa.db.QueryRow("SELECT value FROM registrations WHERE id = ?;", id).Scan(&jsonReg)
if err != nil {
return
}
err = json.Unmarshal(jsonReg, &reg)
return
}
func (ssa *SQLStorageAuthority) GetAuthorization(id string) (authz core.Authorization, err error) {
tx, err := ssa.db.Begin()
if err != nil {
@ -116,28 +159,57 @@ func (ssa *SQLStorageAuthority) GetAuthorization(id string) (authz core.Authoriz
return
}
func (ssa *SQLStorageAuthority) AddCertificate(cert []byte) (id string, err error) {
func (ssa *SQLStorageAuthority) GetCertificate(id string) (cert []byte, err error) {
err = ssa.db.QueryRow("SELECT value FROM certificates WHERE digest = ?;", id).Scan(&cert)
return
}
func (ssa *SQLStorageAuthority) NewRegistration() (id string, err error) {
tx, err := ssa.db.Begin()
if err != nil {
return
}
// Manually set the index, to avoid AUTOINCREMENT issues
var sequence int64
var scanTarget sql.NullInt64
err = tx.QueryRow("SELECT max(sequence) FROM certificates;").Scan(&scanTarget)
switch {
case !scanTarget.Valid:
sequence = 0
case err != nil:
tx.Rollback()
return
default:
sequence += scanTarget.Int64 + 1
// Check that it doesn't exist already
candidate := core.NewToken()
for existingRegistration(tx, candidate) > 0 {
candidate = core.NewToken()
}
id = core.Fingerprint256(cert)
_, err = tx.Exec("INSERT INTO certificates (sequence, digest, value) VALUES (?,?,?);", sequence, id, cert)
// Insert a stub row in pending
_, err = tx.Exec("INSERT INTO registrations (id) VALUES (?);", candidate)
if err != nil {
tx.Rollback()
return
}
if err = tx.Commit(); err != nil {
return
}
id = candidate
return
}
func (ssa *SQLStorageAuthority) UpdateRegistration(reg core.Registration) (err error) {
tx, err := ssa.db.Begin()
if err != nil {
return
}
if existingRegistration(tx, reg.ID) != 1 {
err = errors.New("Requested registration not found " + reg.ID)
tx.Rollback()
return
}
jsonReg, err := json.Marshal(reg)
if err != nil {
tx.Rollback()
return
}
_, err = tx.Exec("UPDATE registrations SET thumbprint=?, value=? WHERE id = ?;", reg.Key.Thumbprint, string(jsonReg), reg.ID)
if err != nil {
tx.Rollback()
return
@ -271,3 +343,34 @@ func (ssa *SQLStorageAuthority) FinalizeAuthorization(authz core.Authorization)
err = tx.Commit()
return
}
func (ssa *SQLStorageAuthority) AddCertificate(cert []byte) (id string, err error) {
tx, err := ssa.db.Begin()
if err != nil {
return
}
// Manually set the index, to avoid AUTOINCREMENT issues
var sequence int64
var scanTarget sql.NullInt64
err = tx.QueryRow("SELECT max(sequence) FROM certificates;").Scan(&scanTarget)
switch {
case !scanTarget.Valid:
sequence = 0
case err != nil:
tx.Rollback()
return
default:
sequence += scanTarget.Int64 + 1
}
id = core.Fingerprint256(cert)
_, err = tx.Exec("INSERT INTO certificates (sequence, digest, value) VALUES (?,?,?);", sequence, id, cert)
if err != nil {
tx.Rollback()
return
}
err = tx.Commit()
return
}

33
test/js/README.md Normal file
View File

@ -0,0 +1,33 @@
# A JS tester for boulder
The node.js scripts in this directory provide a simple end-to-end test of Boulder. (Using some pieces from [node-acme](https://github.com/letsencrypt/node-acme/)) To run:
```
# Install dependencies
> npm install inquirer cli node-forge
# Start cfssl with signing parameters
# (These are the default parameters to use a Yubikey.)
# (You'll need to make your own key, cert, and policy.)
> go install -tags pkcs11 github.com/cloudflare/cfssl/cmd/cfssl
> cfssl serve -port 9000 -ca ca.cert.pem \
-pkcs11-module "/Library/OpenSC/lib/opensc-pkcs11.so" \
-pkcs11-token "Yubico Yubik NEO CCID" \
-pkcs11-pin 123456 \
-pkcs11-label "PIV AUTH key" \
-config policy.json
# Start boulder
# (Change CFSSL parameters to match your setup.)
> go install github.com/letsencrypt/boulder
> boulder-start --cfssl localhost:9000 \
--cfsslProfile ee \
--cfsslAuthKey 79999d86250c367a2b517a1ae7d409c1 \
monolithic
# Client side
> mkdir -p .well-known/acme-challenge/
> node demo.js
> mv -- *.txt .well-known/acme-challenge/ # In a different window
> python -m SimpleHTTPServer 5001 # In yet another window
```

73
test/js/acme-util.js Normal file
View File

@ -0,0 +1,73 @@
// 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/.
module.exports = {
fromStandardB64: function(x) {
return x.replace(/[+]/g, "-").replace(/\//g, "_").replace(/=/g,"");
},
toStandardB64: function(x) {
var b64 = x.replace(/-/g, "+").replace(/_/g, "/").replace(/=/g, "");
switch (b64.length % 4) {
case 2: b64 += "=="; break;
case 3: b64 += "="; break;
}
return b64;
},
b64enc: function(buffer) {
return this.fromStandardB64(buffer.toString("base64"));
},
b64dec: function(str) {
return new Buffer(this.toStandardB64(str), "base64");
},
isB64String: function(x) {
return (typeof(x) == "string") && !x.match(/[^a-zA-Z0-9_-]/);
},
fieldsPresent: function(fields, object) {
for (var i in fields) {
if (!(fields[i] in object)) {
return false;
}
}
return true;
},
validSignature: function(sig) {
return ((typeof(sig) == "object") &&
("alg" in sig) && (typeof(sig.alg) == "string") &&
("nonce" in sig) && this.isB64String(sig.nonce) &&
("sig" in sig) && this.isB64String(sig.sig) &&
("jwk" in sig) && this.validJWK(sig.jwk));
},
validJWK: function(jwk) {
return ((typeof(jwk) == "object") && ("kty" in jwk) && (
((jwk.kty == "RSA")
&& ("n" in jwk) && this.isB64String(jwk.n)
&& ("e" in jwk) && this.isB64String(jwk.e)) ||
((jwk.kty == "EC")
&& ("crv" in jwk)
&& ("x" in jwk) && this.isB64String(jwk.x)
&& ("y" in jwk) && this.isB64String(jwk.y))
) && !("d" in jwk));
},
// A simple, non-standard fingerprint for a JWK,
// just so that we don't have to store objects
keyFingerprint: function(jwk) {
switch (jwk.kty) {
case "RSA": return jwk.n;
case "EC": return jwk.crv + jwk.x + jwk.y;
}
throw "Unrecognized key type";
}
};

346
test/js/crypto-util.js Normal file
View File

@ -0,0 +1,346 @@
// 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/.
var crypto = require("crypto");
var forge = require("node-forge");
var util = require("./acme-util.js");
var TOKEN_SIZE = 16;
var NONCE_SIZE = 16;
function bytesToBuffer(bytes) {
return new Buffer(forge.util.bytesToHex(bytes), "hex");
}
function bufferToBytes(buf) {
return forge.util.hexToBytes(buf.toString("hex"));
}
function bytesToBase64(bytes) {
return util.b64enc(bytesToBuffer(bytes));
}
function base64ToBytes(base64) {
return bufferToBytes(util.b64dec(base64));
}
function bnToBase64(bn) {
var hex = bn.toString(16);
if (hex.length % 2 == 1) { hex = "0" + hex; }
return util.b64enc(new Buffer(hex, "hex"));
}
function base64ToBn(base64) {
return new forge.jsbn.BigInteger(util.b64dec(base64).toString("hex"), 16);
}
function importPrivateKey(privateKey) {
return forge.pki.rsa.setPrivateKey(
base64ToBn(privateKey.n),
base64ToBn(privateKey.e), base64ToBn(privateKey.d),
base64ToBn(privateKey.p), base64ToBn(privateKey.q),
base64ToBn(privateKey.dp),base64ToBn(privateKey.dq),
base64ToBn(privateKey.qi));
}
function importPublicKey(publicKey) {
return forge.pki.rsa.setPublicKey(
base64ToBn(publicKey.n),
base64ToBn(publicKey.e));
}
function exportPrivateKey(privateKey) {
return {
"kty": "RSA",
"n": bnToBase64(privateKey.n),
"e": bnToBase64(privateKey.e),
"d": bnToBase64(privateKey.d),
"p": bnToBase64(privateKey.p),
"q": bnToBase64(privateKey.q),
"dp": bnToBase64(privateKey.dP),
"dq": bnToBase64(privateKey.dQ),
"qi": bnToBase64(privateKey.qInv)
};
}
function exportPublicKey(publicKey) {
return {
"kty": "RSA",
"n": bnToBase64(publicKey.n),
"e": bnToBase64(publicKey.e)
};
}
// A note on formats:
// * Keys are always represented as JWKs
// * Signature objects are in ACME format
// * Certs and CSRs are base64-encoded
module.exports = {
///// RANDOM STRINGS
randomString: function(nBytes) {
return bytesToBase64(forge.random.getBytesSync(nBytes));
},
randomSerialNumber: function() {
return forge.util.bytesToHex(forge.random.getBytesSync(4));
},
newToken: function() {
return this.randomString(TOKEN_SIZE);
},
///// SHA-256
sha256: function(buf) {
return crypto.createHash('sha256').update(buf).digest('hex');
},
///// KEY PAIR MANAGEMENT
generateKeyPair: function(bits) {
var keyPair = forge.pki.rsa.generateKeyPair({bits: bits, e: 0x10001});
return {
privateKey: exportPrivateKey(keyPair.privateKey),
publicKey: exportPublicKey(keyPair.publicKey)
};
},
importPemPrivateKey: function(pem) {
var key = forge.pki.privateKeyFromPem(pem);
return {
privateKey: exportPrivateKey(key),
publicKey: exportPublicKey(key)
};
},
importPemCertificate: function(pem) {
return forge.pki.certificateFromPem(pem);
},
privateKeyToPem: function(privateKey) {
var priv = importPrivateKey(privateKey);
return forge.pki.privateKeyToPem(priv);
},
certificateToPem: function(certificate) {
var derCert = base64ToBytes(certificate);
var cert = forge.pki.certificateFromAsn1(forge.asn1.fromDer(derCert));
return forge.pki.certificateToPem(cert);
},
certificateRequestToPem: function(csr) {
var derReq = base64ToBytes(csr);
var c = forge.pki.certificateFromAsn1(forge.asn1.fromDer(derReq));
return forge.pki.certificateRequestToPem(c);
},
///// SIGNATURE GENERATION / VERIFICATION
generateSignature: function(keyPair, payload) {
var nonce = bytesToBuffer(forge.random.getBytesSync(NONCE_SIZE));
var privateKey = importPrivateKey(keyPair.privateKey);
// Compute JWS signature
var protectedHeader = JSON.stringify({
nonce: util.b64enc(nonce)
});
var protected64 = util.b64enc(new Buffer(protectedHeader));
var payload64 = util.b64enc(payload);
var signatureInputBuf = new Buffer(protected64 + "." + payload64);
var signatureInput = bufferToBytes(signatureInputBuf);
var md = forge.md.sha256.create();
md.update(signatureInput);
var sig = privateKey.sign(md);
return {
header: {
alg: "RS256",
jwk: keyPair.publicKey,
},
protected: protected64,
payload: payload64,
signature: util.b64enc(bytesToBuffer(sig)),
}
},
verifySignature: function(jws) {
if (jws.protected) {
if (!jws.header) {
jws.header = {};
}
try {
console.log(jws.protected);
var protectedJSON = util.b64dec(jws.protected).toString();
console.log(protectedJSON);
var protectedObj = JSON.parse(protectedJSON);
for (key in protectedObj) {
jws.header[key] = protectedObj[key];
}
} catch (e) {
console.log("error unmarshaling json: "+e)
return false;
}
}
// Assumes validSignature(sig)
if (!jws.header.jwk || (jws.header.jwk.kty != "RSA")) {
// Unsupported key type
console.log("Unsupported key type");
return false;
} else if (!jws.header.alg || !jws.header.alg.match(/^RS/)) {
// Unsupported algorithm
console.log("Unsupported alg: "+jws.header.alg);
return false;
}
// Compute signature input
var protected64 = (jws.protected)? jws.protected : "";
var payload64 = (jws.payload)? jws.payload : "";
var signatureInputBuf = new Buffer(protected64 + "." + payload64);
var signatureInput = bufferToBytes(signatureInputBuf);
// Compute message digest
var md;
switch (jws.header.alg) {
case "RS1": md = forge.md.sha1.create(); break;
case "RS256": md = forge.md.sha256.create(); break;
case "RS384": md = forge.md.sha384.create(); break;
case "RS512": md = forge.md.sha512.create(); break;
default: return false; // Unsupported algorithm
}
md.update(signatureInput);
// Import the key and signature
var publicKey = importPublicKey(jws.header.jwk);
var sig = bufferToBytes(util.b64dec(jws.signature));
return publicKey.verify(md.digest().bytes(), sig);
},
///// CSR GENERATION / VERIFICATION
generateCSR: function(keyPair, identifier) {
var privateKey = importPrivateKey(keyPair.privateKey);
var publicKey = importPublicKey(keyPair.publicKey);
// Create and sign the CSR
var csr = forge.pki.createCertificationRequest();
csr.publicKey = publicKey;
csr.setSubject([{ name: 'commonName', value: identifier }]);
csr.sign(privateKey);
// Convert CSR -> DER -> Base64
var der = forge.asn1.toDer(forge.pki.certificationRequestToAsn1(csr));
return util.b64enc(bytesToBuffer(der));
},
verifiedCommonName: function(csr_b64) {
var der = bufferToBytes(util.b64dec(csr_b64));
var csr = forge.pki.certificationRequestFromAsn1(forge.asn1.fromDer(der));
if (!csr.verify()) {
return false;
}
for (var i=0; i<csr.subject.attributes.length; ++i) {
if (csr.subject.attributes[i].name == "commonName") {
return csr.subject.attributes[i].value;
}
}
return false;
},
///// CERTIFICATE GENERATION
// 'ca' parameter includes information about the CA
// {
// distinguishedName: /* forge-formatted DN */
// keyPair: {
// publicKey: /* JWK */
// privateKey: /* JWK */
// }
// }
generateCertificate: function(ca, serialNumber, csr_b64) {
var der = bufferToBytes(util.b64dec(csr_b64));
var csr = forge.pki.certificationRequestFromAsn1(forge.asn1.fromDer(der));
// Extract the public key and common name
var publicKey = csr.publicKey;
var commonName = null;
for (var i=0; i<csr.subject.attributes.length; ++i) {
if (csr.subject.attributes[i].name == "commonName") {
commonName = csr.subject.attributes[i].value;
break;
}
}
if (!commonName) { return false; }
// Create the certificate
var cert = forge.pki.createCertificate();
cert.publicKey = publicKey;
cert.serialNumber = serialNumber;
// 1-year validity
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
cert.setSubject([{ name: "commonName", value: commonName }]);
cert.setIssuer(ca.distinguishedName);
cert.setExtensions([
{ name: "basicConstraints", cA: false },
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true },
{ name: "extKeyUsage", serverAuth: true },
{ name: "subjectAltName", altNames: [{ type: 2, value: commonName }] }
]);
// Import signing key and sign
var privateKey = importPrivateKey(ca.keyPair.privateKey);
cert.sign(privateKey);
// Return base64-encoded DER
var der = forge.asn1.toDer(forge.pki.certificateToAsn1(cert));
return bytesToBuffer(der);
},
generateDvsniCertificate: function(keyPair, nonceName, zName) {
var cert = forge.pki.createCertificate();
cert.publicKey = importPublicKey(keyPair.publicKey);
cert.serialNumber = '01';
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
cert.setSubject([{ name: "commonName", value: nonceName }]);
cert.setIssuer([{ name: "commonName", value: nonceName }]);
cert.setExtensions([
{ name: "basicConstraints", cA: false },
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true },
{ name: "extKeyUsage", serverAuth: true },
{ name: "subjectAltName", altNames: [
{ type: 2, value: nonceName },
{ type: 2, value: zName }
]}
]);
cert.sign(importPrivateKey(keyPair.privateKey));
// Return base64-encoded DER, as above
var der = forge.asn1.toDer(forge.pki.certificateToAsn1(cert));
return util.b64enc(bytesToBuffer(der));
},
///// TLS CONTEXT GENERATION
createContext: function(keyPair, cert) {
var privateKey = importPrivateKey(keyPair.privateKey);
var derCert = bufferToBytes(util.b64dec(cert));
var realCert = forge.pki.certificateFromAsn1(forge.asn1.fromDer(derCert));
return crypto.createCredentials({
key: forge.pki.privateKeyToPem(privateKey),
cert: forge.pki.certificateToPem(realCert)
}).context;
}
};

445
test/js/test.js Normal file
View File

@ -0,0 +1,445 @@
// 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/.
"use strict";
var inquirer = require("inquirer");
var cli = require("cli");
var http = require('http');
var fs = require('fs');
var url = require('url');
var util = require("./acme-util");
var crypto = require("./crypto-util");
var questions = {
email: [{
type: "input",
name: "email",
message: "Please enter your email address (for recovery purposes)",
validate: function(value) {
var pass = value.match(/[\w.+-]+@[\w.-]+/i);
if (pass) {
return true;
} else {
return "Please enter a valid email address";
}
}
}],
terms: [{
type: "confirm",
name: "terms",
message: "Do you agree to these terms?",
default: false,
}],
domain: [{
type: "input",
name: "domain",
message: "Please enter the domain name for the certificate",
validate: function(value) {
var pass = value.match(/[\w.-]+/i);
if (pass) {
return true;
} else {
return "Please enter a valid domain name";
}
}
}],
readyToValidate: [{
type: "input",
name: "noop",
message: "Press enter to when you're ready to proceed",
}],
files: [{
type: "input",
name: "keyFile",
message: "Name for key file",
default: "key.pem"
},{
type: "input",
name: "certFile",
message: "Name for certificate file",
default: "cert.pem"
}],
};
var state = {
keyPairBits: 512,
keyPair: null,
newRegistrationURL: "http://localhost:4000/acme/new-reg",
registrationURL: "",
termsRequired: false,
termsAgreed: false,
termsURL: null,
domain: null,
newAuthorizationURL: "",
authorizationURL: "",
responseURL: "",
path: "",
retryDelay: 1000,
newCertificateURL: "",
certificateURL: "",
};
function parseLink(link) {
try {
// NB: Takes last among links with the same "rel" value
var links = link.split(',').map(function(link) {
var parts = link.trim().split(";");
var url = parts.shift().replace(/[<>]/g, "");
var info = parts.reduce(function(acc, p) {
var m = p.trim().match(/(.+) *= *"(.+)"/);
if (m) acc[m[1]] = m[2];
return acc
}, {});
info["url"] = url;
return info;
}).reduce(function(acc, link) {
if ("rel" in link) {
acc[link["rel"]] = link["url"]
}
return acc;
}, {});
return links;
} catch (e) {
return null;
}
}
/*
The asynchronous nature of node.js libraries makes the control flow a
little hard to follow here, but it pretty much goes straight down the
page, with detours through the `inquirer` and `http` libraries.
main
|
register
|
getTerms
| \
| getAgreement
| |
| sendAgreement
| /
getDomain
|
getChallenges
|
getReadyToValidate
|
sendResponse
|
ensureValidation
|
getCertificate
|
downloadCertificate
|
saveFiles
*/
function main() {
console.log("Generating key pair...");
state.keyPair = crypto.generateKeyPair(state.keyPairBits);
console.log();
inquirer.prompt(questions.email, register)
}
function register(answers) {
var email = answers.email;
// Register public key
state.registration = {
contact: [ "mailto:" + email ]
}
var registerMessage = JSON.stringify(state.registration);
var jws = crypto.generateSignature(state.keyPair, new Buffer(registerMessage));
var payload = JSON.stringify(jws);
var options = url.parse(state.newRegistrationURL);
options.method = "POST";
var req = http.request(options, getTerms);
req.write(payload)
req.end();
}
function getTerms(resp) {
if (Math.floor(resp.statusCode / 100) != 2) {
// Non-2XX response
console.log("Registration request failed with code " + resp.statusCode);
return;
}
var links = parseLink(resp.headers["link"]);
if (!links || !("next" in links)) {
console.log("The server did not provide information to proceed");
return
}
state.registrationURL = resp.headers["location"];
state.newAuthorizationURL = links["next"];
state.termsRequired = ("terms-of-service" in links);
if (state.termsRequired) {
state.termsURL = links["terms-of-service"];
http.get(state.termsURL, getAgreement)
} else {
inquirer.prompt(questions.domain, getChallenges);
}
}
function getAgreement(resp) {
var body = "";
resp.on("data", function(chunk) {
body += chunk;
});
resp.on("end", function(chunk) {
if (chunk) { body += chunk; }
// TODO: Check content-type
console.log("The CA requires your agreement to terms (not supported).");
console.log();
console.log(body);
console.log();
inquirer.prompt(questions.terms, sendAgreement);
});
}
function sendAgreement(answers) {
state.termsAgreed = answers.terms;
if (state.termsRequired && !state.termsAgreed) {
console.log("Sorry, can't proceed if you don't agree.");
process.exit(1);
}
state.registration.agreement = state.termsURL;
var registerMessage = JSON.stringify(state.registration);
var jws = crypto.generateSignature(state.keyPair, new Buffer(registerMessage));
var payload = JSON.stringify(jws);
console.log("Posting agreement to: " + state.registrationURL)
var options = url.parse(state.registrationURL);
options.method = "POST";
var req = http.request(options, function(resp) {
var body = "";
resp.on("data", function(chunk) { body += chunk; });
resp.on("end", function() {
if (Math.floor(resp.statusCode / 100) != 2) {
// Non-2XX response
console.log("Couldn't POST agreement back to server, aborting.");
console.log("Code: "+ resp.statusCode);
console.log(body);
process.exit(1);
}
});
inquirer.prompt(questions.domain, getChallenges);
});
req.write(payload)
req.end();
}
function getChallenges(answers) {
state.domain = answers.domain;
// Register public key
var authzMessage = JSON.stringify({
identifier: {
type: "dns",
value: state.domain
}
});
var jws = crypto.generateSignature(state.keyPair, new Buffer(authzMessage));
var payload = JSON.stringify(jws);
var options = url.parse(state.newAuthorizationURL);
options.method = "POST";
var req = http.request(options, getReadyToValidate);
req.write(payload)
req.end();
}
function getReadyToValidate(resp) {
if (Math.floor(resp.statusCode / 100) != 2) {
// Non-2XX response
console.log("Authorization request failed with code " + resp.statusCode)
return;
}
var links = parseLink(resp.headers["link"]);
if (!links || !("next" in links)) {
console.log("The server did not provide information to proceed");
return
}
state.authorizationURL = resp.headers["location"];
state.newCertificateURL = links["next"];
var body = ""
resp.on('data', function(chunk) {
body += chunk;
});
resp.on('end', function(chunk) {
if (chunk) { body += chunk; }
var authz = JSON.parse(body);
var simpleHttps = authz.challenges.filter(function(x) { return x.type == "simpleHttps"; });
if (simpleHttps.length == 0) {
console.log("The server didn't offer any challenges we can handle.");
return;
}
var challenge = simpleHttps[0];
var path = crypto.randomString(8) + ".txt";
fs.writeFileSync(path, challenge.token);
state.responseURL = challenge["uri"];
state.path = path;
console.log();
console.log("To validate that you own "+ state.domain +", the CA has\n" +
"asked you to provision a file on your server. I've saved\n" +
"the file here for you.\n");
console.log(" File: " + path);
console.log(" URL: http://"+ state.domain +"/.well-known/acme-challenge/"+ path);
console.log();
// To do this locally (boulder connects to port 5001)
// > mkdir -p .well-known/acme-challenge/
// > mv $CHALLENGE_FILE ./well-known/acme-challenge/
// > python -m SimpleHTTPServer 5001
inquirer.prompt(questions.readyToValidate, sendResponse);
});
}
function sendResponse() {
var responseMessage = JSON.stringify({
path: state.path
});
var jws = crypto.generateSignature(state.keyPair, new Buffer(responseMessage));
var payload = JSON.stringify(jws);
cli.spinner("Validating domain");
var options = url.parse(state.responseURL);
options.method = "POST";
var req = http.request(options, ensureValidation);
req.write(payload)
req.end();
}
function ensureValidation(resp) {
if (Math.floor(resp.statusCode / 100) != 2) {
// Non-2XX response
console.log("Authorization status request failed with code " + resp.statusCode)
return;
}
var body = "";
resp.on('data', function(chunk) {
body += chunk;
});
resp.on('end', function(chunk) {
if (chunk) { body += chunk; }
var authz = JSON.parse(body);
if (authz.status == "pending") {
setTimeout(function() {
http.get(state.authorizationURL, ensureValidation);
}, state.retryDelay);
} else if (authz.status == "valid") {
cli.spinner("Validating domain ... done", true);
console.log();
getCertificate();
} else if (authz.status == "invalid") {
console.log("The CA was unable to validate the file you provisioned.");
return;
} else {
console.log("The CA returned an authorization in an unexpected state");
console.log(JSON.stringify(authz, null, " "));
return;
}
});
}
function getCertificate() {
var csr = crypto.generateCSR(state.keyPair, state.domain);
var certificateMessage = JSON.stringify({
csr: csr,
authorizations: [ state.authorizationURL ]
});
var jws = crypto.generateSignature(state.keyPair, new Buffer(certificateMessage));
var payload = JSON.stringify(jws);
cli.spinner("Requesting certificate");
var options = url.parse(state.newCertificateURL);
options.method = "POST";
var req = http.request(options, downloadCertificate);
req.write(payload)
req.end();
}
function downloadCertificate(resp) {
var chunks = [];
resp.on('data', function(chunk) {
chunks.push(chunk);
});
resp.on('end', function(chunk) {
if (chunk) { chunks.push(chunk); }
var body = Buffer.concat(chunks);
if (Math.floor(resp.statusCode / 100) != 2) {
// Non-2XX response
console.log("Certificate request failed with code " + resp.statusCode);
console.log(body.toString());
return;
}
cli.spinner("Requesting certificate ... done", true);
console.log();
var certB64 = util.b64enc(body);
state.certificate = certB64;
inquirer.prompt(questions.files, saveFiles);
});
}
function saveFiles(answers) {
var keyPEM = crypto.privateKeyToPem(state.keyPair.privateKey);
fs.writeFileSync(answers.keyFile, keyPEM);
var certPEM = crypto.certificateToPem(state.certificate);
fs.writeFileSync(answers.certFile, certPEM);
console.log("Done!")
console.log("To try it out:");
console.log("openssl s_server -accept 8080 -www -key "+
answers.keyFile +" -cert "+ answers.certFile);
// XXX: Explicitly exit, since something's tenacious here
process.exit(0);
}
// BEGIN
main();

View File

@ -28,14 +28,8 @@ func NewValidationAuthorityImpl() ValidationAuthorityImpl {
// Validation methods
func (va ValidationAuthorityImpl) validateSimpleHTTPS(authz core.Authorization) (challenge core.Challenge) {
identifier := authz.Identifier.Value
challenge, ok := authz.Challenges[core.ChallengeTypeSimpleHTTPS]
if !ok {
challenge.Status = core.StatusInvalid
return
}
func (va ValidationAuthorityImpl) validateSimpleHTTPS(identifier core.AcmeIdentifier, input core.Challenge) (challenge core.Challenge) {
challenge = input
if len(challenge.Path) == 0 {
challenge.Status = core.StatusInvalid
@ -52,7 +46,7 @@ func (va ValidationAuthorityImpl) validateSimpleHTTPS(authz core.Authorization)
return
}
httpRequest.Host = identifier
httpRequest.Host = identifier.Value
client := http.Client{Timeout: 5 * time.Second}
httpResponse, err := client.Do(httpRequest)
@ -74,14 +68,8 @@ func (va ValidationAuthorityImpl) validateSimpleHTTPS(authz core.Authorization)
return
}
func (va ValidationAuthorityImpl) validateDvsni(authz core.Authorization) (challenge core.Challenge) {
// identifier := authz.Identifier.Value // XXX: Local version; uncomment for real version
challenge, ok := authz.Challenges[core.ChallengeTypeDVSNI]
if !ok {
challenge.Status = core.StatusInvalid
return
}
func (va ValidationAuthorityImpl) validateDvsni(identifier core.AcmeIdentifier, input core.Challenge) (challenge core.Challenge) {
challenge = input
const DVSNI_SUFFIX = ".acme.invalid"
nonceName := challenge.Nonce + DVSNI_SUFFIX
@ -139,13 +127,13 @@ func (va ValidationAuthorityImpl) validateDvsni(authz core.Authorization) (chall
func (va ValidationAuthorityImpl) validate(authz core.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)
for i, challenge := range authz.Challenges {
switch challenge.Type {
case core.ChallengeTypeSimpleHTTPS:
authz.Challenges[i] = va.validateSimpleHTTPS(authz.Identifier, challenge)
break
case "dvsni":
authz.Challenges[i] = va.validateDvsni(authz)
case core.ChallengeTypeDVSNI:
authz.Challenges[i] = va.validateDvsni(authz.Identifier, challenge)
break
}
}

View File

@ -10,6 +10,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"regexp"
"github.com/letsencrypt/boulder/core"
@ -21,9 +22,14 @@ type WebFrontEndImpl struct {
SA core.StorageGetter
// URL configuration parameters
baseURL string
authzBase string
certBase string
NewReg string
RegBase string
NewAuthz string
AuthzBase string
NewCert string
CertBase string
SubscriberAgreementURL string
}
func NewWebFrontEndImpl() WebFrontEndImpl {
@ -88,15 +94,11 @@ func sendError(response http.ResponseWriter, message string, code int) {
http.Error(response, string(problemDoc), code)
}
func (wfe *WebFrontEndImpl) SetAuthzBase(base string) {
wfe.authzBase = base
func link(url, relation string) string {
return fmt.Sprintf("<%s>;rel=\"%s\"", url, relation)
}
func (wfe *WebFrontEndImpl) SetCertBase(base string) {
wfe.certBase = base
}
func (wfe *WebFrontEndImpl) NewAuthz(response http.ResponseWriter, request *http.Request) {
func (wfe *WebFrontEndImpl) NewRegistration(response http.ResponseWriter, request *http.Request) {
if request.Method != "POST" {
sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
return
@ -104,7 +106,51 @@ func (wfe *WebFrontEndImpl) NewAuthz(response http.ResponseWriter, request *http
body, key, err := verifyPOST(request)
if err != nil {
sendError(response, "Unable to read body", http.StatusBadRequest)
sendError(response, fmt.Sprintf("Unable to read/verify body: %v", err), http.StatusBadRequest)
return
}
var init core.Registration
err = json.Unmarshal(body, &init)
if err != nil {
sendError(response, "Error unmarshaling JSON", http.StatusBadRequest)
return
}
reg, err := wfe.RA.NewRegistration(init, key)
if err != nil {
sendError(response,
fmt.Sprintf("Error creating new registration: %+v", err),
http.StatusInternalServerError)
}
regURL := wfe.RegBase + string(reg.ID)
reg.ID = ""
responseBody, err := json.Marshal(reg)
if err != nil {
sendError(response, "Error marshaling authz", http.StatusInternalServerError)
return
}
response.Header().Add("Location", regURL)
response.Header().Add("Link", link(wfe.NewAuthz, "next"))
if len(wfe.SubscriberAgreementURL) > 0 {
response.Header().Add("Link", link(wfe.SubscriberAgreementURL, "terms-of-service"))
}
response.WriteHeader(http.StatusCreated)
response.Write(responseBody)
}
func (wfe *WebFrontEndImpl) NewAuthorization(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/verify body", http.StatusBadRequest)
return
}
@ -115,7 +161,7 @@ func (wfe *WebFrontEndImpl) NewAuthz(response http.ResponseWriter, request *http
return
}
// TODO: Create new authz and return
// Create new authz and return
authz, err := wfe.RA.NewAuthorization(init, key)
if err != nil {
sendError(response,
@ -125,7 +171,7 @@ func (wfe *WebFrontEndImpl) NewAuthz(response http.ResponseWriter, request *http
}
// Make a URL for this authz, then blow away the ID before serializing
authzURL := wfe.authzBase + string(authz.ID)
authzURL := wfe.AuthzBase + string(authz.ID)
authz.ID = ""
responseBody, err := json.Marshal(authz)
if err != nil {
@ -134,11 +180,12 @@ func (wfe *WebFrontEndImpl) NewAuthz(response http.ResponseWriter, request *http
}
response.Header().Add("Location", authzURL)
response.Header().Add("Link", link(wfe.NewCert, "next"))
response.WriteHeader(http.StatusCreated)
response.Write(responseBody)
}
func (wfe *WebFrontEndImpl) NewCert(response http.ResponseWriter, request *http.Request) {
func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request *http.Request) {
if request.Method != "POST" {
sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
return
@ -146,7 +193,7 @@ func (wfe *WebFrontEndImpl) NewCert(response http.ResponseWriter, request *http.
body, key, err := verifyPOST(request)
if err != nil {
sendError(response, "Unable to read body", http.StatusBadRequest)
sendError(response, "Unable to read/verify body", http.StatusBadRequest)
return
}
@ -167,7 +214,7 @@ func (wfe *WebFrontEndImpl) NewCert(response http.ResponseWriter, request *http.
}
// Make a URL for this authz
certURL := wfe.certBase + string(cert.ID)
certURL := wfe.CertBase + string(cert.ID)
// TODO: Content negotiation for cert format
response.Header().Add("Location", certURL)
@ -175,13 +222,22 @@ func (wfe *WebFrontEndImpl) NewCert(response http.ResponseWriter, request *http.
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)
authz, err := wfe.SA.GetAuthorization(id)
if err != nil {
func (wfe *WebFrontEndImpl) Challenge(authz core.Authorization, response http.ResponseWriter, request *http.Request) {
// Check that the requested challenge exists within the authorization
found := false
var challengeIndex int
for i, challenge := range authz.Challenges {
tempURL := url.URL(challenge.URI)
if tempURL.Path == request.URL.Path && tempURL.RawQuery == request.URL.RawQuery {
found = true
challengeIndex = i
break
}
}
if !found {
sendError(response,
fmt.Sprintf("Unable to find authorization: %+v", err),
fmt.Sprintf("Unable to find challenge"),
http.StatusNotFound)
return
}
@ -194,12 +250,12 @@ func (wfe *WebFrontEndImpl) Authz(response http.ResponseWriter, request *http.Re
case "POST":
body, key, err := verifyPOST(request)
if err != nil {
sendError(response, "Unable to read body", http.StatusBadRequest)
sendError(response, "Unable to read/verify body", http.StatusBadRequest)
return
}
var initialAuthz core.Authorization
err = json.Unmarshal(body, &initialAuthz)
var challengeResponse core.Challenge
err = json.Unmarshal(body, &challengeResponse)
if err != nil {
sendError(response, "Error unmarshaling authorization", http.StatusBadRequest)
return
@ -207,15 +263,12 @@ func (wfe *WebFrontEndImpl) Authz(response http.ResponseWriter, request *http.Re
// 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)
updatedAuthz, err := wfe.RA.UpdateAuthorization(authz, challengeIndex, challengeResponse)
if err != nil {
sendError(response, "Unable to update authorization", http.StatusInternalServerError)
return
@ -229,6 +282,96 @@ func (wfe *WebFrontEndImpl) Authz(response http.ResponseWriter, request *http.Re
response.WriteHeader(http.StatusAccepted)
response.Write(jsonReply)
}
}
func (wfe *WebFrontEndImpl) Registration(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)
reg, err := wfe.SA.GetRegistration(id)
if err != nil {
sendError(response,
fmt.Sprintf("Unable to find registration: %+v", err),
http.StatusNotFound)
return
}
reg.ID = id
switch request.Method {
default:
sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
return
case "GET":
jsonReply, err := json.Marshal(reg)
if err != nil {
sendError(response, "Failed to marshal authz", http.StatusInternalServerError)
return
}
response.WriteHeader(http.StatusOK)
response.Write(jsonReply)
case "POST":
body, key, err := verifyPOST(request)
if err != nil {
sendError(response, "Unable to read/verify body", http.StatusBadRequest)
return
}
var update core.Registration
err = json.Unmarshal(body, &update)
if err != nil {
sendError(response, "Error unmarshaling registration", http.StatusBadRequest)
return
}
// Check that the signing key is the right key
if !key.Equals(reg.Key) {
sendError(response, "Signing key does not match key in registration", http.StatusForbidden)
return
}
// Ask the RA to update this authorization
updatedReg, err := wfe.RA.UpdateRegistration(reg, update)
if err != nil {
fmt.Println(err)
sendError(response, "Unable to update registration", http.StatusInternalServerError)
return
}
jsonReply, err := json.Marshal(updatedReg)
if err != nil {
sendError(response, "Failed to marshal authz", http.StatusInternalServerError)
return
}
response.WriteHeader(http.StatusAccepted)
response.Write(jsonReply)
}
}
func (wfe *WebFrontEndImpl) Authorization(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)
authz, err := wfe.SA.GetAuthorization(id)
if err != nil {
sendError(response,
fmt.Sprintf("Unable to find authorization: %+v", err),
http.StatusNotFound)
return
}
// If there is a fragment, then this is actually a request to a challenge URI
if len(request.URL.RawQuery) != 0 {
wfe.Challenge(authz, response, request)
return
}
switch request.Method {
default:
sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
return
case "GET":
jsonReply, err := json.Marshal(authz)
if err != nil {
@ -240,7 +383,7 @@ func (wfe *WebFrontEndImpl) Authz(response http.ResponseWriter, request *http.Re
}
}
func (wfe *WebFrontEndImpl) Cert(response http.ResponseWriter, request *http.Request) {
func (wfe *WebFrontEndImpl) Certificate(response http.ResponseWriter, request *http.Request) {
switch request.Method {
default:
sendError(response, "Method not allowed", http.StatusMethodNotAllowed)