Merge pull request #35 from bifurcation/spec-update
Updating to be compatible with current acme-spec.
This commit is contained in:
commit
5a268b4149
|
|
@ -34,8 +34,9 @@ type CertificateAuthorityImpl struct {
|
||||||
func NewCertificateAuthorityImpl(hostport string, authKey string, profile string) (ca *CertificateAuthorityImpl, err error) {
|
func NewCertificateAuthorityImpl(hostport string, authKey string, profile string) (ca *CertificateAuthorityImpl, err error) {
|
||||||
// Create the remote signer
|
// Create the remote signer
|
||||||
localProfile := config.SigningProfile{
|
localProfile := config.SigningProfile{
|
||||||
Expiry: 60 * time.Minute, // BOGUS: Required by CFSSL, but not used
|
Expiry: time.Hour, // BOGUS: Required by CFSSL, but not used
|
||||||
RemoteName: hostport,
|
RemoteName: hostport, // BOGUS: Only used as a flag by CFSSL
|
||||||
|
RemoteServer: hostport,
|
||||||
}
|
}
|
||||||
|
|
||||||
localProfile.Provider, err = auth.New(authKey, nil)
|
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?
|
// XXX Take in authorizations and verify that union covers CSR?
|
||||||
// Pull hostnames from CSR
|
// Pull hostnames from CSR
|
||||||
hostNames := csr.DNSNames // DNSNames + CN 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
|
var commonName string
|
||||||
if len(csr.Subject.CommonName) > 0 {
|
if len(csr.Subject.CommonName) > 0 {
|
||||||
commonName = csr.Subject.CommonName
|
commonName = csr.Subject.CommonName
|
||||||
} else {
|
} else if len(hostNames) > 0 {
|
||||||
commonName = 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
|
// Convert the CSR to PEM
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,16 @@ package ca
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"encoding/asn1"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"net/http"
|
||||||
"testing"
|
"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/cloudflare/cfssl/signer/local"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
|
||||||
|
|
@ -108,24 +114,109 @@ var CA_CERT_PEM = "-----BEGIN CERTIFICATE-----\n" +
|
||||||
// * Random public key
|
// * Random public key
|
||||||
// * CN = example.com
|
// * CN = example.com
|
||||||
// * DNSNames = example.com, www.example.com
|
// * DNSNames = example.com, www.example.com
|
||||||
var CSR_HEX = "308202953082017d0201003016311430120603" +
|
var CN_AND_SAN_CSR_HEX = "308202a130820189020100301a311830160603550403130f6e6f742d6578" +
|
||||||
"550403130b6578616d706c652e636f6d30820122300d06092a864886f70d0101010500038201" +
|
"616d706c652e636f6d30820122300d06092a864886f70d01010105000382" +
|
||||||
"0f003082010a0282010100baaf16e891828470cad87b849a73356f65e20ad3699fd5583a7200" +
|
"010f003082010a0282010100e56ccbe37003c150202e6f543f9eb1d0e590" +
|
||||||
"e924512d9eeb1dbe16441ad7bd804fa2e5726a06f0af5279012fe6354a5677259f5591984aa9" +
|
"76ac7f1f62654fa82fe131a23c66bd53a2f62ff7852015c84a394e36836d" +
|
||||||
"99b8ea3ea10fbd5ecfa30e5f563b41c419374decfc98ea62c611046ad011c326470a2426f46d" +
|
"2018eba278e0740c85c4c6102787400c2ef069b4a72e6eb8ad8d1da5d76b" +
|
||||||
"be6cc44fae3b247e19710810585f9f3ad7f64b2f305aebb72e2829866f89b20b03a300b7ff5f" +
|
"f3e70dafc126578ed28cf40030e7fe5b5307ef630254726c639561b5445d" +
|
||||||
"4e6204f41420d9fa731252692cee8e616636723abe8a7053fd86e2673190fa8b618ada5bc735" +
|
"372847bdb02576aa3622a688158c6af09d3938dbeba4d670cec4325be73a" +
|
||||||
"ba57a145af86904a8f55a288d4d6ba9e501530f23f197f5b623443193fc92b7f87d6abbf740d" +
|
"fa52a0a04dcba2f335f1e85020704db94ca125dce70b3209294c6c46ed4b" +
|
||||||
"9fc92800c7e0e1484d5eec6ffae1007c139c1ec19d67e749743fe8d8396fe190cfbcf2f90e05" +
|
"48b95d8d51ae2d2fd227116023a48ca7381e35fd302ad2999df625a4b5ee" +
|
||||||
"230203010001a03a303806092a864886f70d01090e312b302930270603551d110420301e820b" +
|
"82a0d0fefa88ac6a62b01674de75637ef83328202cda9930947d932000b0" +
|
||||||
"6578616d706c652e636f6d820f7777772e6578616d706c652e636f6d300d06092a864886f70d" +
|
"e53b82e099ab60fec9c8b6d4eccdee508b6ebca7e6ca3f752046c8350203" +
|
||||||
"01010b05000382010100514c622dc866b31c86777a26e9b2911618ce5b188591f08007b42772" +
|
"010001a042304006092a864886f70d01090e31333031302f0603551d1104" +
|
||||||
"3497b733676a7d493c434fc819b8089869442fd299aa99ff7f7b9df881843727f0c8b89ca62a" +
|
"283026820f6e6f742d6578616d706c652e636f6d82137777772e6e6f742d" +
|
||||||
"f8a12b38c963e9210148c4e1c0fc964aef2605f88ed777e6497d2e43e9a9713835d1ae96260c" +
|
"6578616d706c652e636f6d300d06092a864886f70d01010b050003820101" +
|
||||||
"ca826c34c7ae52c77f5d8468643ee1936eadf04e1ebf8bbbb68b0ec7d0ef694432451292e4a3" +
|
"008c4bf2ab4dfd28d768697eecc5be889a6275287c7dd24f9232ffad5675" +
|
||||||
"1989fd8339c07e01e04b6dd3834872b828d3f5b2b4dadda0596396e15fbdf446998066a74873" +
|
"de708c9cc911545d0e84f61b6584c5e237915bbf231d6518e7e228be2e65" +
|
||||||
"2baf53f3f7ebb849e83cf734753c35ab454d1b62e1741a6514c5c575c0c805b4d668fcf71746" +
|
"b4d50bd9729ce9e6aee00482e014de4edd4b9a4f9a7777b8943ef3512dbf" +
|
||||||
"ef32017613a52d6b807e2977f4fbc0a88b2e263347c4d9e35435333cf4f8288be53df41228ec"
|
"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) {
|
func TestIssueCertificate(t *testing.T) {
|
||||||
// Decode pre-generated values
|
// Decode pre-generated values
|
||||||
|
|
@ -135,28 +226,70 @@ func TestIssueCertificate(t *testing.T) {
|
||||||
caCertPEM, _ := pem.Decode([]byte(CA_CERT_PEM))
|
caCertPEM, _ := pem.Decode([]byte(CA_CERT_PEM))
|
||||||
caCert, _ := x509.ParseCertificate(caCertPEM.Bytes)
|
caCert, _ := x509.ParseCertificate(caCertPEM.Bytes)
|
||||||
|
|
||||||
csrDER, _ := hex.DecodeString(CSR_HEX)
|
// Uncomment to create a CFSSL local signer
|
||||||
csr, _ := x509.ParseCertificateRequest(csrDER)
|
|
||||||
|
|
||||||
// Create a CFSSL local signer
|
// CFSSL config
|
||||||
signer, _ := local.NewSigner(caKey, caCert, x509.SHA256WithRSA, nil)
|
hostPort := "localhost:9000"
|
||||||
|
authKey := "79999d86250c367a2b517a1ae7d409c1"
|
||||||
|
profileName := "ee"
|
||||||
|
|
||||||
// Create an SA
|
// Create an SA
|
||||||
sa, err := sa.NewSQLStorageAuthority("sqlite3", ":memory:")
|
sa, err := sa.NewSQLStorageAuthority("sqlite3", ":memory:")
|
||||||
test.AssertNotError(t, err, "Failed to create SA")
|
test.AssertNotError(t, err, "Failed to create SA")
|
||||||
sa.InitTables()
|
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
|
// Create a CA
|
||||||
/*
|
|
||||||
// Uncomment to test with a remote signer
|
// 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")
|
test.AssertNotError(t, err, "Failed to create CA")
|
||||||
ca.SA = sa
|
ca.SA = sa
|
||||||
*/
|
|
||||||
|
/*
|
||||||
|
// Uncomment to test with a local signer
|
||||||
|
signer, _ := local.NewSigner(caKey, caCert, x509.SHA256WithRSA, nil)
|
||||||
ca := CertificateAuthorityImpl{
|
ca := CertificateAuthorityImpl{
|
||||||
Signer: signer,
|
Signer: signer,
|
||||||
SA: sa,
|
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
|
// Sign CSR
|
||||||
certObj, err := ca.IssueCertificate(*csr)
|
certObj, err := ca.IssueCertificate(*csr)
|
||||||
|
|
@ -166,13 +299,27 @@ func TestIssueCertificate(t *testing.T) {
|
||||||
cert, err := x509.ParseCertificate(certObj.DER)
|
cert, err := x509.ParseCertificate(certObj.DER)
|
||||||
test.AssertNotError(t, err, "Certificate failed to parse")
|
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)
|
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
|
// Verify that the cert got stored in the DB
|
||||||
_, err = sa.GetCertificate(certObj.ID)
|
_, err = sa.GetCertificate(certObj.ID)
|
||||||
test.AssertNotError(t, err, "Certificate not found in database")
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,6 @@ func main() {
|
||||||
app.Usage = "Command-line utility to start Boulder's servers in stand-alone mode"
|
app.Usage = "Command-line utility to start Boulder's servers in stand-alone mode"
|
||||||
app.Version = "0.0.0"
|
app.Version = "0.0.0"
|
||||||
|
|
||||||
// Specify AMQP Server
|
|
||||||
app.Flags = []cli.Flag{
|
app.Flags = []cli.Flag{
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "amqp",
|
Name: "amqp",
|
||||||
|
|
@ -97,12 +96,6 @@ func main() {
|
||||||
Usage: "Start the CA in monolithic mode, without using AMQP",
|
Usage: "Start the CA in monolithic mode, without using AMQP",
|
||||||
Action: func(c *cli.Context) {
|
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
|
// Grab parameters
|
||||||
cfsslServer := c.GlobalString("cfssl")
|
cfsslServer := c.GlobalString("cfssl")
|
||||||
authKey := c.GlobalString("cfsslAuthKey")
|
authKey := c.GlobalString("cfsslAuthKey")
|
||||||
|
|
@ -112,6 +105,8 @@ func main() {
|
||||||
wfe := wfe.NewWebFrontEndImpl()
|
wfe := wfe.NewWebFrontEndImpl()
|
||||||
sa, err := sa.NewSQLStorageAuthority("sqlite3", ":memory:")
|
sa, err := sa.NewSQLStorageAuthority("sqlite3", ":memory:")
|
||||||
failOnError(err, "Unable to create SA")
|
failOnError(err, "Unable to create SA")
|
||||||
|
err = sa.InitTables()
|
||||||
|
failOnError(err, "Unable to initialize SA")
|
||||||
ra := ra.NewRegistrationAuthorityImpl()
|
ra := ra.NewRegistrationAuthorityImpl()
|
||||||
va := va.NewValidationAuthorityImpl()
|
va := va.NewValidationAuthorityImpl()
|
||||||
ca, err := ca.NewCertificateAuthorityImpl(cfsslServer, authKey, profile)
|
ca, err := ca.NewCertificateAuthorityImpl(cfsslServer, authKey, profile)
|
||||||
|
|
@ -124,17 +119,40 @@ func main() {
|
||||||
ra.SA = sa
|
ra.SA = sa
|
||||||
ra.VA = &va
|
ra.VA = &va
|
||||||
va.RA = &ra
|
va.RA = &ra
|
||||||
|
ca.SA = sa
|
||||||
|
|
||||||
// Go!
|
// Go!
|
||||||
authority := "0.0.0.0:4000"
|
authority := "0.0.0.0:4000"
|
||||||
|
urlBase := "http://" + authority
|
||||||
|
newRegPath := "/acme/new-reg"
|
||||||
|
regPath := "/acme/reg/"
|
||||||
|
newAuthzPath := "/acme/new-authz"
|
||||||
authzPath := "/acme/authz/"
|
authzPath := "/acme/authz/"
|
||||||
|
newCertPath := "/acme/new-cert"
|
||||||
certPath := "/acme/cert/"
|
certPath := "/acme/cert/"
|
||||||
wfe.SetAuthzBase("http://" + authority + authzPath)
|
wfe.NewReg = urlBase + newRegPath
|
||||||
wfe.SetCertBase("http://" + authority + certPath)
|
wfe.RegBase = urlBase + regPath
|
||||||
http.HandleFunc("/acme/new-authz", wfe.NewAuthz)
|
wfe.NewAuthz = urlBase + newAuthzPath
|
||||||
http.HandleFunc("/acme/new-cert", wfe.NewCert)
|
wfe.AuthzBase = urlBase + authzPath
|
||||||
http.HandleFunc("/acme/authz/", wfe.Authz)
|
wfe.NewCert = urlBase + newCertPath
|
||||||
http.HandleFunc("/acme/cert/", wfe.Cert)
|
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")
|
fmt.Fprintf(os.Stderr, "Server running...\n")
|
||||||
err = http.ListenAndServe(authority, nil)
|
err = http.ListenAndServe(authority, nil)
|
||||||
|
|
@ -201,14 +219,25 @@ func main() {
|
||||||
|
|
||||||
// Go!
|
// Go!
|
||||||
authority := "0.0.0.0:4000"
|
authority := "0.0.0.0:4000"
|
||||||
|
urlBase := "http://" + authority
|
||||||
|
newRegPath := "/acme/new-reg"
|
||||||
|
regPath := "/acme/reg/"
|
||||||
|
newAuthzPath := "/acme/new-authz"
|
||||||
authzPath := "/acme/authz/"
|
authzPath := "/acme/authz/"
|
||||||
|
newCertPath := "/acme/new-cert"
|
||||||
certPath := "/acme/cert/"
|
certPath := "/acme/cert/"
|
||||||
wfe.SetAuthzBase("http://" + authority + authzPath)
|
wfe.NewReg = urlBase + newRegPath
|
||||||
wfe.SetCertBase("http://" + authority + certPath)
|
wfe.RegBase = urlBase + regPath
|
||||||
http.HandleFunc("/acme/new-authz", wfe.NewAuthz)
|
wfe.NewAuthz = urlBase + newAuthzPath
|
||||||
http.HandleFunc("/acme/new-cert", wfe.NewCert)
|
wfe.AuthzBase = urlBase + authzPath
|
||||||
http.HandleFunc("/acme/authz/", wfe.Authz)
|
wfe.NewCert = urlBase + newCertPath
|
||||||
http.HandleFunc("/acme/cert/", wfe.Cert)
|
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")
|
fmt.Fprintf(os.Stderr, "Server running...\n")
|
||||||
err = http.ListenAndServe(authority, nil)
|
err = http.ListenAndServe(authority, nil)
|
||||||
|
|
@ -235,14 +264,25 @@ func main() {
|
||||||
|
|
||||||
// Connect the front end to HTTP
|
// Connect the front end to HTTP
|
||||||
authority := "0.0.0.0:4000"
|
authority := "0.0.0.0:4000"
|
||||||
|
urlBase := "http://" + authority
|
||||||
|
newRegPath := "/acme/new-reg"
|
||||||
|
regPath := "/acme/reg/"
|
||||||
|
newAuthzPath := "/acme/new-authz"
|
||||||
authzPath := "/acme/authz/"
|
authzPath := "/acme/authz/"
|
||||||
|
newCertPath := "/acme/new-cert"
|
||||||
certPath := "/acme/cert/"
|
certPath := "/acme/cert/"
|
||||||
wfe.SetAuthzBase("http://" + authority + authzPath)
|
wfe.NewReg = urlBase + newRegPath
|
||||||
wfe.SetCertBase("http://" + authority + certPath)
|
wfe.RegBase = urlBase + regPath
|
||||||
http.HandleFunc("/acme/new-authz", wfe.NewAuthz)
|
wfe.NewAuthz = urlBase + newAuthzPath
|
||||||
http.HandleFunc("/acme/new-cert", wfe.NewCert)
|
wfe.AuthzBase = urlBase + authzPath
|
||||||
http.HandleFunc("/acme/authz/", wfe.Authz)
|
wfe.NewCert = urlBase + newCertPath
|
||||||
http.HandleFunc("/acme/cert/", wfe.Cert)
|
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")
|
fmt.Fprintf(os.Stderr, "Server running...\n")
|
||||||
http.ListenAndServe(authority, nil)
|
http.ListenAndServe(authority, nil)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
|
|
||||||
func SimpleHTTPSChallenge() Challenge {
|
func SimpleHTTPSChallenge() Challenge {
|
||||||
return Challenge{
|
return Challenge{
|
||||||
|
Type: ChallengeTypeSimpleHTTPS,
|
||||||
Status: StatusPending,
|
Status: StatusPending,
|
||||||
Token: NewToken(),
|
Token: NewToken(),
|
||||||
}
|
}
|
||||||
|
|
@ -21,6 +22,7 @@ func DvsniChallenge() Challenge {
|
||||||
nonce := make([]byte, 16)
|
nonce := make([]byte, 16)
|
||||||
rand.Read(nonce)
|
rand.Read(nonce)
|
||||||
return Challenge{
|
return Challenge{
|
||||||
|
Type: ChallengeTypeDVSNI,
|
||||||
Status: StatusPending,
|
Status: StatusPending,
|
||||||
R: RandomString(32),
|
R: RandomString(32),
|
||||||
Nonce: hex.EncodeToString(nonce),
|
Nonce: hex.EncodeToString(nonce),
|
||||||
|
|
|
||||||
|
|
@ -246,6 +246,9 @@ func TestURL(t *testing.T) {
|
||||||
url.URL.Path != path || url.URL.RawQuery != query {
|
url.URL.Path != path || url.URL.RawQuery != query {
|
||||||
t.Errorf("Improper URL contents: %v", url.URL)
|
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)
|
err = json.Unmarshal([]byte(badJSON), &url)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,18 @@ type WebFrontEnd interface {
|
||||||
// Set the base URL for certificates
|
// Set the base URL for certificates
|
||||||
SetCertBase(path string)
|
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
|
// This method represents the ACME new-authorization resource
|
||||||
NewAuthz(response http.ResponseWriter, request *http.Request)
|
NewAuthz(response http.ResponseWriter, request *http.Request)
|
||||||
|
|
||||||
// This method represents the ACME new-certificate resource
|
// This method represents the ACME new-certificate resource
|
||||||
NewCert(response http.ResponseWriter, request *http.Request)
|
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
|
// Provide access to requests for authorization resources
|
||||||
Authz(response http.ResponseWriter, request *http.Request)
|
Authz(response http.ResponseWriter, request *http.Request)
|
||||||
|
|
||||||
|
|
@ -43,6 +49,9 @@ type WebFrontEnd interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type RegistrationAuthority interface {
|
type RegistrationAuthority interface {
|
||||||
|
// [WebFrontEnd]
|
||||||
|
NewRegistration(Registration, jose.JsonWebKey) (Registration, error)
|
||||||
|
|
||||||
// [WebFrontEnd]
|
// [WebFrontEnd]
|
||||||
NewAuthorization(Authorization, jose.JsonWebKey) (Authorization, error)
|
NewAuthorization(Authorization, jose.JsonWebKey) (Authorization, error)
|
||||||
|
|
||||||
|
|
@ -50,7 +59,10 @@ type RegistrationAuthority interface {
|
||||||
NewCertificate(CertificateRequest, jose.JsonWebKey) (Certificate, error)
|
NewCertificate(CertificateRequest, jose.JsonWebKey) (Certificate, error)
|
||||||
|
|
||||||
// [WebFrontEnd]
|
// [WebFrontEnd]
|
||||||
UpdateAuthorization(Authorization) (Authorization, error)
|
UpdateRegistration(Registration, Registration) (Registration, error)
|
||||||
|
|
||||||
|
// [WebFrontEnd]
|
||||||
|
UpdateAuthorization(Authorization, int, Challenge) (Authorization, error)
|
||||||
|
|
||||||
// [WebFrontEnd]
|
// [WebFrontEnd]
|
||||||
RevokeCertificate(x509.Certificate) error
|
RevokeCertificate(x509.Certificate) error
|
||||||
|
|
@ -70,15 +82,20 @@ type CertificateAuthority interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type StorageGetter interface {
|
type StorageGetter interface {
|
||||||
GetCertificate(string) ([]byte, error)
|
GetRegistration(string) (Registration, error)
|
||||||
GetAuthorization(string) (Authorization, error)
|
GetAuthorization(string) (Authorization, error)
|
||||||
|
GetCertificate(string) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type StorageAdder interface {
|
type StorageAdder interface {
|
||||||
AddCertificate([]byte) (string, error)
|
NewRegistration() (string, error)
|
||||||
|
UpdateRegistration(Registration) error
|
||||||
|
|
||||||
NewPendingAuthorization() (string, error)
|
NewPendingAuthorization() (string, error)
|
||||||
UpdatePendingAuthorization(Authorization) error
|
UpdatePendingAuthorization(Authorization) error
|
||||||
FinalizeAuthorization(Authorization) error
|
FinalizeAuthorization(Authorization) error
|
||||||
|
|
||||||
|
AddCertificate([]byte) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The StorageAuthority interface represnts a simple key/value
|
// The StorageAuthority interface represnts a simple key/value
|
||||||
|
|
|
||||||
|
|
@ -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
|
// Rather than define individual types for different types of
|
||||||
// challenge, we just throw all the elements into one bucket,
|
// challenge, we just throw all the elements into one bucket,
|
||||||
// together with the common metadata elements.
|
// together with the common metadata elements.
|
||||||
type Challenge struct {
|
type Challenge struct {
|
||||||
|
// The type of challenge
|
||||||
|
Type string `json:"type"`
|
||||||
|
|
||||||
// The status of this challenge
|
// The status of this challenge
|
||||||
Status AcmeStatus `json:"status,omitempty"`
|
Status AcmeStatus `json:"status,omitempty"`
|
||||||
|
|
||||||
|
|
@ -99,6 +132,9 @@ type Challenge struct {
|
||||||
// was completed by the server.
|
// was completed by the server.
|
||||||
Completed time.Time `json:"completed,omitempty"`
|
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
|
// Used by simpleHttps, recoveryToken, and dns challenges
|
||||||
Token string `json:"token,omitempty"`
|
Token string `json:"token,omitempty"`
|
||||||
|
|
||||||
|
|
@ -112,6 +148,7 @@ type Challenge struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge a client-provide response to a challenge with the issued challenge
|
// 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 {
|
func (ch Challenge) MergeResponse(resp Challenge) Challenge {
|
||||||
// Only override fields that are supposed to be client-provided
|
// Only override fields that are supposed to be client-provided
|
||||||
if len(ch.Path) == 0 {
|
if len(ch.Path) == 0 {
|
||||||
|
|
@ -153,11 +190,11 @@ type Authorization struct {
|
||||||
// in process, these are challenges to be fulfilled; for
|
// in process, these are challenges to be fulfilled; for
|
||||||
// final authorizations, they describe the evidence that
|
// final authorizations, they describe the evidence that
|
||||||
// the server used in support of granting the authorization.
|
// 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
|
// The server may suggest combinations of challenges if it
|
||||||
// requires more than one challenge to be completed.
|
// 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
|
// The client may provide contact URIs to allow the server
|
||||||
// to push information to it.
|
// to push information to it.
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,14 @@ func Fingerprint256(data []byte) string {
|
||||||
// URLs that automatically marshal/unmarshal to JSON strings
|
// URLs that automatically marshal/unmarshal to JSON strings
|
||||||
type AcmeURL url.URL
|
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) {
|
func (u AcmeURL) MarshalJSON() ([]byte, error) {
|
||||||
uu := url.URL(u)
|
uu := url.URL(u)
|
||||||
return json.Marshal(uu.String())
|
return json.Marshal(uu.String())
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ package ra
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -22,6 +23,8 @@ type RegistrationAuthorityImpl struct {
|
||||||
CA core.CertificateAuthority
|
CA core.CertificateAuthority
|
||||||
VA core.ValidationAuthority
|
VA core.ValidationAuthority
|
||||||
SA core.StorageAuthority
|
SA core.StorageAuthority
|
||||||
|
|
||||||
|
AuthzBase string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRegistrationAuthorityImpl() RegistrationAuthorityImpl {
|
func NewRegistrationAuthorityImpl() RegistrationAuthorityImpl {
|
||||||
|
|
@ -83,6 +86,24 @@ func lastPathSegment(url core.AcmeURL) string {
|
||||||
return allButLastPathSegment.ReplaceAllString(url.Path, "")
|
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) {
|
func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization, key jose.JsonWebKey) (authz core.Authorization, err error) {
|
||||||
identifier := request.Identifier
|
identifier := request.Identifier
|
||||||
|
|
||||||
|
|
@ -99,9 +120,18 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create validations
|
// Create validations
|
||||||
|
// TODO: Assign URLs
|
||||||
simpleHttps := core.SimpleHTTPSChallenge()
|
simpleHttps := core.SimpleHTTPSChallenge()
|
||||||
dvsni := core.DvsniChallenge()
|
dvsni := core.DvsniChallenge()
|
||||||
authID, err := ra.SA.NewPendingAuthorization()
|
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
|
// Create a new authorization object
|
||||||
authz = core.Authorization{
|
authz = core.Authorization{
|
||||||
|
|
@ -109,9 +139,9 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization
|
||||||
Identifier: identifier,
|
Identifier: identifier,
|
||||||
Key: key,
|
Key: key,
|
||||||
Status: core.StatusPending,
|
Status: core.StatusPending,
|
||||||
Challenges: map[string]core.Challenge{
|
Challenges: []core.Challenge{
|
||||||
core.ChallengeTypeSimpleHTTPS: simpleHttps,
|
simpleHttps,
|
||||||
core.ChallengeTypeDVSNI: dvsni,
|
dvsni,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,37 +197,29 @@ func (ra *RegistrationAuthorityImpl) NewCertificate(req core.CertificateRequest,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ra *RegistrationAuthorityImpl) UpdateAuthorization(delta core.Authorization) (authz core.Authorization, err error) {
|
func (ra *RegistrationAuthorityImpl) UpdateRegistration(base core.Registration, update core.Registration) (reg core.Registration, err error) {
|
||||||
// Fetch the copy of this authorization we have on file
|
base.MergeUpdate(update)
|
||||||
authz, err = ra.SA.GetAuthorization(delta.ID)
|
reg = base
|
||||||
if err != nil {
|
err = ra.SA.UpdateRegistration(base)
|
||||||
return
|
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
|
// Copy information over that the client is allowed to supply
|
||||||
if len(delta.Contact) > 0 {
|
authz = base
|
||||||
authz.Contact = delta.Contact
|
if challengeIndex >= len(authz.Challenges) {
|
||||||
}
|
err = core.MalformedRequestError("Invalid challenge index")
|
||||||
newResponse := false
|
return
|
||||||
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
|
// Store the updated version
|
||||||
ra.SA.UpdatePendingAuthorization(authz)
|
ra.SA.UpdatePendingAuthorization(authz)
|
||||||
|
|
||||||
// If any challenges were updated, dispatch to the VA for service
|
// Dispatch to the VA for service
|
||||||
if newResponse {
|
|
||||||
ra.VA.UpdateValidations(authz)
|
ra.VA.UpdateValidations(authz)
|
||||||
}
|
|
||||||
|
|
||||||
return authz, nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ra *RegistrationAuthorityImpl) RevokeCertificate(cert x509.Certificate) error {
|
func (ra *RegistrationAuthorityImpl) RevokeCertificate(cert x509.Certificate) error {
|
||||||
|
|
|
||||||
|
|
@ -120,15 +120,9 @@ var (
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthzDelta = core.Authorization{
|
ResponseIndex = 0
|
||||||
Challenges: map[string]core.Challenge{
|
Response = core.Challenge{
|
||||||
core.ChallengeTypeSimpleHTTPS: core.Challenge{
|
|
||||||
Path: "Hf5GrX4Q7EBax9hc2jJnfw",
|
Path: "Hf5GrX4Q7EBax9hc2jJnfw",
|
||||||
},
|
|
||||||
core.ChallengeTypeDVSNI: core.Challenge{
|
|
||||||
S: "23029d88d9e123e",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ExampleCSR = &x509.CertificateRequest{}
|
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.Identifier == AuthzRequest.Identifier, "Initial authz had wrong identifier")
|
||||||
test.Assert(t, authz.Status == core.StatusPending, "Initial authz not pending")
|
test.Assert(t, authz.Status == core.StatusPending, "Initial authz not pending")
|
||||||
|
|
||||||
_, ok := authz.Challenges[core.ChallengeTypeDVSNI]
|
// TODO Verify challenges
|
||||||
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")
|
|
||||||
|
|
||||||
// If we get to here, we'll use this authorization for the next test
|
// If we get to here, we'll use this authorization for the next test
|
||||||
AuthzInitial = authz
|
AuthzInitial = authz
|
||||||
|
|
@ -209,9 +200,8 @@ func TestUpdateAuthorization(t *testing.T) {
|
||||||
_, va, sa, ra := initAuthorities(t)
|
_, va, sa, ra := initAuthorities(t)
|
||||||
AuthzInitial.ID, _ = sa.NewPendingAuthorization()
|
AuthzInitial.ID, _ = sa.NewPendingAuthorization()
|
||||||
sa.UpdatePendingAuthorization(AuthzInitial)
|
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")
|
test.AssertNotError(t, err, "UpdateAuthorization failed")
|
||||||
|
|
||||||
// Verify that returned authz same as DB
|
// Verify that returned authz same as DB
|
||||||
|
|
@ -224,14 +214,9 @@ func TestUpdateAuthorization(t *testing.T) {
|
||||||
assertAuthzEqual(t, authz, va.Argument)
|
assertAuthzEqual(t, authz, va.Argument)
|
||||||
|
|
||||||
// Verify that the responses are reflected
|
// Verify that the responses are reflected
|
||||||
simpleHttps, ok := va.Argument.Challenges[core.ChallengeTypeSimpleHTTPS]
|
test.Assert(t, len(va.Argument.Challenges) > 0, "Authz passed to VA has no challenges")
|
||||||
simpleHttpsOrig, _ := AuthzDelta.Challenges[core.ChallengeTypeSimpleHTTPS]
|
simpleHttps := va.Argument.Challenges[0]
|
||||||
test.Assert(t, ok, "Authz passed to VA has no simpleHttps challenge")
|
test.Assert(t, simpleHttps.Path == Response.Path, "simpleHttps changed")
|
||||||
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")
|
|
||||||
|
|
||||||
// If we get to here, we'll use this authorization for the next test
|
// If we get to here, we'll use this authorization for the next test
|
||||||
AuthzUpdated = authz
|
AuthzUpdated = authz
|
||||||
|
|
@ -247,9 +232,7 @@ func TestOnValidationUpdate(t *testing.T) {
|
||||||
|
|
||||||
// Simulate a successful simpleHttps challenge
|
// Simulate a successful simpleHttps challenge
|
||||||
AuthzFromVA = AuthzUpdated
|
AuthzFromVA = AuthzUpdated
|
||||||
challenge := AuthzFromVA.Challenges[core.ChallengeTypeSimpleHTTPS]
|
AuthzFromVA.Challenges[0].Status = core.StatusValid
|
||||||
challenge.Status = core.StatusValid
|
|
||||||
AuthzFromVA.Challenges[core.ChallengeTypeSimpleHTTPS] = challenge
|
|
||||||
|
|
||||||
ra.OnValidationUpdate(AuthzFromVA)
|
ra.OnValidationUpdate(AuthzFromVA)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,19 +33,22 @@ import (
|
||||||
// so it doesn't need wrappers.
|
// so it doesn't need wrappers.
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
MethodNewRegistration = "NewRegistration" // RA, SA
|
||||||
MethodNewAuthorization = "NewAuthorization" // RA
|
MethodNewAuthorization = "NewAuthorization" // RA
|
||||||
MethodNewCertificate = "NewCertificate" // RA
|
MethodNewCertificate = "NewCertificate" // RA
|
||||||
|
MethodUpdateRegistration = "UpdateRegistration" // RA, SA
|
||||||
MethodUpdateAuthorization = "UpdateAuthorization" // RA
|
MethodUpdateAuthorization = "UpdateAuthorization" // RA
|
||||||
MethodRevokeCertificate = "RevokeCertificate" // RA
|
MethodRevokeCertificate = "RevokeCertificate" // RA
|
||||||
MethodOnValidationUpdate = "OnValidationUpdate" // RA
|
MethodOnValidationUpdate = "OnValidationUpdate" // RA
|
||||||
MethodUpdateValidations = "UpdateValidations" // VA
|
MethodUpdateValidations = "UpdateValidations" // VA
|
||||||
MethodIssueCertificate = "IssueCertificate" // CA
|
MethodIssueCertificate = "IssueCertificate" // CA
|
||||||
MethodGetCertificate = "GetCertificate" // SA
|
MethodGetRegistration = "GetRegistration" // SA
|
||||||
MethodGetAuthorization = "GetAuthorization" // SA
|
MethodGetAuthorization = "GetAuthorization" // SA
|
||||||
MethodAddCertificate = "AddCertificate" // SA
|
MethodGetCertificate = "GetCertificate" // SA
|
||||||
MethodNewPendingAuthorization = "NewPendingAuthorization" // SA
|
MethodNewPendingAuthorization = "NewPendingAuthorization" // SA
|
||||||
MethodUpdatePendingAuthorization = "UpdatePendingAuthorization" // SA
|
MethodUpdatePendingAuthorization = "UpdatePendingAuthorization" // SA
|
||||||
MethodFinalizeAuthorization = "FinalizeAuthorization" // SA
|
MethodFinalizeAuthorization = "FinalizeAuthorization" // SA
|
||||||
|
MethodAddCertificate = "AddCertificate" // SA
|
||||||
)
|
)
|
||||||
|
|
||||||
// RegistrationAuthorityClient / Server
|
// RegistrationAuthorityClient / Server
|
||||||
|
|
@ -54,6 +57,11 @@ const (
|
||||||
// -> UpdateAuthorization
|
// -> UpdateAuthorization
|
||||||
// -> RevokeCertificate
|
// -> RevokeCertificate
|
||||||
// -> OnValidationUpdate
|
// -> OnValidationUpdate
|
||||||
|
type registrationRequest struct {
|
||||||
|
Reg core.Registration
|
||||||
|
Key jose.JsonWebKey
|
||||||
|
}
|
||||||
|
|
||||||
type authorizationRequest struct {
|
type authorizationRequest struct {
|
||||||
Authz core.Authorization
|
Authz core.Authorization
|
||||||
Key jose.JsonWebKey
|
Key jose.JsonWebKey
|
||||||
|
|
@ -67,6 +75,25 @@ type certificateRequest struct {
|
||||||
func NewRegistrationAuthorityServer(serverQueue string, channel *amqp.Channel, impl core.RegistrationAuthority) (rpc *AmqpRpcServer, err error) {
|
func NewRegistrationAuthorityServer(serverQueue string, channel *amqp.Channel, impl core.RegistrationAuthority) (rpc *AmqpRpcServer, err error) {
|
||||||
rpc = NewAmqpRpcServer(serverQueue, channel)
|
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) {
|
rpc.Handle(MethodNewAuthorization, func(req []byte) (response []byte) {
|
||||||
var ar authorizationRequest
|
var ar authorizationRequest
|
||||||
err := json.Unmarshal(req, &ar)
|
err := json.Unmarshal(req, &ar)
|
||||||
|
|
@ -111,14 +138,39 @@ func NewRegistrationAuthorityServer(serverQueue string, channel *amqp.Channel, i
|
||||||
return
|
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) {
|
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)
|
err := json.Unmarshal(req, &authz)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newAuthz, err := impl.UpdateAuthorization(authz)
|
newAuthz, err := impl.UpdateAuthorization(authz.Authz, authz.Index, authz.Response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -174,6 +226,21 @@ func NewRegistrationAuthorityClient(clientQueue, serverQueue string, channel *am
|
||||||
return
|
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) {
|
func (rac RegistrationAuthorityClient) NewAuthorization(authz core.Authorization, key jose.JsonWebKey) (newAuthz core.Authorization, err error) {
|
||||||
data, err := json.Marshal(authorizationRequest{authz, key})
|
data, err := json.Marshal(authorizationRequest{authz, key})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -204,8 +271,36 @@ func (rac RegistrationAuthorityClient) NewCertificate(cr core.CertificateRequest
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rac RegistrationAuthorityClient) UpdateAuthorization(authz core.Authorization) (newAuthz core.Authorization, err error) {
|
func (rac RegistrationAuthorityClient) UpdateRegistration(base core.Registration, update core.Registration) (newReg core.Registration, err error) {
|
||||||
data, err := json.Marshal(authz)
|
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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -337,11 +432,17 @@ func (cac CertificateAuthorityClient) IssueCertificate(csr x509.CertificateReque
|
||||||
func NewStorageAuthorityServer(serverQueue string, channel *amqp.Channel, impl core.StorageAuthority) (rpc *AmqpRpcServer) {
|
func NewStorageAuthorityServer(serverQueue string, channel *amqp.Channel, impl core.StorageAuthority) (rpc *AmqpRpcServer) {
|
||||||
rpc = NewAmqpRpcServer(serverQueue, channel)
|
rpc = NewAmqpRpcServer(serverQueue, channel)
|
||||||
|
|
||||||
rpc.Handle(MethodGetCertificate, func(req []byte) (response []byte) {
|
rpc.Handle(MethodGetRegistration, func(req []byte) (response []byte) {
|
||||||
cert, err := impl.GetCertificate(string(req))
|
reg, err := impl.GetCertificate(string(req))
|
||||||
if err == nil {
|
if err != nil {
|
||||||
response = []byte(cert)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jsonReg, err := json.Marshal(reg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response = jsonReg
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -366,6 +467,14 @@ func NewStorageAuthorityServer(serverQueue string, channel *amqp.Channel, impl c
|
||||||
return
|
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) {
|
rpc.Handle(MethodNewPendingAuthorization, func(req []byte) (response []byte) {
|
||||||
id, err := impl.NewPendingAuthorization()
|
id, err := impl.NewPendingAuthorization()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
@ -385,17 +494,6 @@ func NewStorageAuthorityServer(serverQueue string, channel *amqp.Channel, impl c
|
||||||
return
|
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) {
|
rpc.Handle(MethodFinalizeAuthorization, func(req []byte) (response []byte) {
|
||||||
var authz core.Authorization
|
var authz core.Authorization
|
||||||
err := json.Unmarshal(req, authz)
|
err := json.Unmarshal(req, authz)
|
||||||
|
|
@ -407,6 +505,14 @@ func NewStorageAuthorityServer(serverQueue string, channel *amqp.Channel, impl c
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
|
rpc.Handle(MethodGetCertificate, func(req []byte) (response []byte) {
|
||||||
|
cert, err := impl.GetCertificate(string(req))
|
||||||
|
if err == nil {
|
||||||
|
response = []byte(cert)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,8 +530,13 @@ func NewStorageAuthorityClient(clientQueue, serverQueue string, channel *amqp.Ch
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cac StorageAuthorityClient) GetCertificate(id string) (cert []byte, err error) {
|
func (cac StorageAuthorityClient) GetRegistration(id string) (reg core.Registration, err error) {
|
||||||
cert, err = cac.rpc.DispatchSync(MethodGetCertificate, []byte(id))
|
jsonReg, err := cac.rpc.DispatchSync(MethodGetRegistration, []byte(id))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(jsonReg, ®)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -439,10 +550,26 @@ func (cac StorageAuthorityClient) GetAuthorization(id string) (authz core.Author
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cac StorageAuthorityClient) AddCertificate(cert []byte) (id string, err error) {
|
func (cac StorageAuthorityClient) GetCertificate(id string) (cert []byte, err error) {
|
||||||
response, err := cac.rpc.DispatchSync(MethodAddCertificate, cert)
|
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 {
|
if err != nil || len(response) == 0 {
|
||||||
err = errors.New("AddCertificate RPC failed") // XXX
|
err = errors.New("NewRegistration RPC failed") // XXX
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
id = string(response)
|
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) {
|
func (cac StorageAuthorityClient) NewPendingAuthorization() (id string, err error) {
|
||||||
response, err := cac.rpc.DispatchSync(MethodNewPendingAuthorization, []byte{})
|
response, err := cac.rpc.DispatchSync(MethodNewPendingAuthorization, []byte{})
|
||||||
if err != nil || len(response) == 0 {
|
if err != nil || len(response) == 0 {
|
||||||
err = errors.New("AddCertificate RPC failed") // XXX
|
err = errors.New("NewPendingAuthorization RPC failed") // XXX
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
id = string(response)
|
id = string(response)
|
||||||
|
|
@ -480,3 +607,13 @@ func (cac StorageAuthorityClient) FinalizeAuthorization(authz core.Authorization
|
||||||
_, err = cac.rpc.DispatchSync(MethodFinalizeAuthorization, jsonAuthz)
|
_, err = cac.rpc.DispatchSync(MethodFinalizeAuthorization, jsonAuthz)
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@ package sa
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/letsencrypt/boulder/core"
|
"github.com/letsencrypt/boulder/core"
|
||||||
)
|
)
|
||||||
|
|
@ -47,8 +49,8 @@ func (ssa *SQLStorageAuthority) InitTables() (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create certificates table
|
// Create registrations table
|
||||||
_, err = tx.Exec("CREATE TABLE certificates (sequence INTEGER, digest TEXT, value BLOB);")
|
_, err = tx.Exec("CREATE TABLE registrations (id TEXT, thumbprint TEXT, value TEXT);")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return
|
return
|
||||||
|
|
@ -68,13 +70,38 @@ func (ssa *SQLStorageAuthority) InitTables() (err error) {
|
||||||
return
|
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()
|
err = tx.Commit()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ssa *SQLStorageAuthority) GetCertificate(id string) (cert []byte, err error) {
|
func (ssa *SQLStorageAuthority) dumpTables(tx *sql.Tx) {
|
||||||
err = ssa.db.QueryRow("SELECT value FROM certificates WHERE digest = ?;", id).Scan(&cert)
|
fmt.Printf("===== TABLE DUMP =====\n")
|
||||||
return
|
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 {
|
func statusIsPending(status core.AcmeStatus) bool {
|
||||||
|
|
@ -91,6 +118,22 @@ func existingFinal(tx *sql.Tx, id string) (count int64) {
|
||||||
return
|
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, ®)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (ssa *SQLStorageAuthority) GetAuthorization(id string) (authz core.Authorization, err error) {
|
func (ssa *SQLStorageAuthority) GetAuthorization(id string) (authz core.Authorization, err error) {
|
||||||
tx, err := ssa.db.Begin()
|
tx, err := ssa.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -116,28 +159,57 @@ func (ssa *SQLStorageAuthority) GetAuthorization(id string) (authz core.Authoriz
|
||||||
return
|
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()
|
tx, err := ssa.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manually set the index, to avoid AUTOINCREMENT issues
|
// Check that it doesn't exist already
|
||||||
var sequence int64
|
candidate := core.NewToken()
|
||||||
var scanTarget sql.NullInt64
|
for existingRegistration(tx, candidate) > 0 {
|
||||||
err = tx.QueryRow("SELECT max(sequence) FROM certificates;").Scan(&scanTarget)
|
candidate = core.NewToken()
|
||||||
switch {
|
|
||||||
case !scanTarget.Valid:
|
|
||||||
sequence = 0
|
|
||||||
case err != nil:
|
|
||||||
tx.Rollback()
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
sequence += scanTarget.Int64 + 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
id = core.Fingerprint256(cert)
|
// Insert a stub row in pending
|
||||||
_, err = tx.Exec("INSERT INTO certificates (sequence, digest, value) VALUES (?,?,?);", sequence, id, cert)
|
_, 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 {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return
|
return
|
||||||
|
|
@ -271,3 +343,34 @@ func (ssa *SQLStorageAuthority) FinalizeAuthorization(authz core.Authorization)
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
```
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
@ -28,14 +28,8 @@ func NewValidationAuthorityImpl() ValidationAuthorityImpl {
|
||||||
|
|
||||||
// Validation methods
|
// Validation methods
|
||||||
|
|
||||||
func (va ValidationAuthorityImpl) validateSimpleHTTPS(authz core.Authorization) (challenge core.Challenge) {
|
func (va ValidationAuthorityImpl) validateSimpleHTTPS(identifier core.AcmeIdentifier, input core.Challenge) (challenge core.Challenge) {
|
||||||
identifier := authz.Identifier.Value
|
challenge = input
|
||||||
|
|
||||||
challenge, ok := authz.Challenges[core.ChallengeTypeSimpleHTTPS]
|
|
||||||
if !ok {
|
|
||||||
challenge.Status = core.StatusInvalid
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(challenge.Path) == 0 {
|
if len(challenge.Path) == 0 {
|
||||||
challenge.Status = core.StatusInvalid
|
challenge.Status = core.StatusInvalid
|
||||||
|
|
@ -52,7 +46,7 @@ func (va ValidationAuthorityImpl) validateSimpleHTTPS(authz core.Authorization)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
httpRequest.Host = identifier
|
httpRequest.Host = identifier.Value
|
||||||
client := http.Client{Timeout: 5 * time.Second}
|
client := http.Client{Timeout: 5 * time.Second}
|
||||||
httpResponse, err := client.Do(httpRequest)
|
httpResponse, err := client.Do(httpRequest)
|
||||||
|
|
||||||
|
|
@ -74,14 +68,8 @@ func (va ValidationAuthorityImpl) validateSimpleHTTPS(authz core.Authorization)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (va ValidationAuthorityImpl) validateDvsni(authz core.Authorization) (challenge core.Challenge) {
|
func (va ValidationAuthorityImpl) validateDvsni(identifier core.AcmeIdentifier, input core.Challenge) (challenge core.Challenge) {
|
||||||
// identifier := authz.Identifier.Value // XXX: Local version; uncomment for real version
|
challenge = input
|
||||||
challenge, ok := authz.Challenges[core.ChallengeTypeDVSNI]
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
challenge.Status = core.StatusInvalid
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const DVSNI_SUFFIX = ".acme.invalid"
|
const DVSNI_SUFFIX = ".acme.invalid"
|
||||||
nonceName := challenge.Nonce + DVSNI_SUFFIX
|
nonceName := challenge.Nonce + DVSNI_SUFFIX
|
||||||
|
|
@ -139,13 +127,13 @@ func (va ValidationAuthorityImpl) validateDvsni(authz core.Authorization) (chall
|
||||||
func (va ValidationAuthorityImpl) validate(authz core.Authorization) {
|
func (va ValidationAuthorityImpl) validate(authz core.Authorization) {
|
||||||
// Select the first supported validation method
|
// Select the first supported validation method
|
||||||
// XXX: Remove the "break" lines to process all supported validations
|
// XXX: Remove the "break" lines to process all supported validations
|
||||||
for i := range authz.Challenges {
|
for i, challenge := range authz.Challenges {
|
||||||
switch i {
|
switch challenge.Type {
|
||||||
case "simpleHttps":
|
case core.ChallengeTypeSimpleHTTPS:
|
||||||
authz.Challenges[i] = va.validateSimpleHTTPS(authz)
|
authz.Challenges[i] = va.validateSimpleHTTPS(authz.Identifier, challenge)
|
||||||
break
|
break
|
||||||
case "dvsni":
|
case core.ChallengeTypeDVSNI:
|
||||||
authz.Challenges[i] = va.validateDvsni(authz)
|
authz.Challenges[i] = va.validateDvsni(authz.Identifier, challenge)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/letsencrypt/boulder/core"
|
"github.com/letsencrypt/boulder/core"
|
||||||
|
|
@ -21,9 +22,14 @@ type WebFrontEndImpl struct {
|
||||||
SA core.StorageGetter
|
SA core.StorageGetter
|
||||||
|
|
||||||
// URL configuration parameters
|
// URL configuration parameters
|
||||||
baseURL string
|
NewReg string
|
||||||
authzBase string
|
RegBase string
|
||||||
certBase string
|
NewAuthz string
|
||||||
|
AuthzBase string
|
||||||
|
NewCert string
|
||||||
|
CertBase string
|
||||||
|
|
||||||
|
SubscriberAgreementURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWebFrontEndImpl() WebFrontEndImpl {
|
func NewWebFrontEndImpl() WebFrontEndImpl {
|
||||||
|
|
@ -88,15 +94,11 @@ func sendError(response http.ResponseWriter, message string, code int) {
|
||||||
http.Error(response, string(problemDoc), code)
|
http.Error(response, string(problemDoc), code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wfe *WebFrontEndImpl) SetAuthzBase(base string) {
|
func link(url, relation string) string {
|
||||||
wfe.authzBase = base
|
return fmt.Sprintf("<%s>;rel=\"%s\"", url, relation)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wfe *WebFrontEndImpl) SetCertBase(base string) {
|
func (wfe *WebFrontEndImpl) NewRegistration(response http.ResponseWriter, request *http.Request) {
|
||||||
wfe.certBase = base
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wfe *WebFrontEndImpl) NewAuthz(response http.ResponseWriter, request *http.Request) {
|
|
||||||
if request.Method != "POST" {
|
if request.Method != "POST" {
|
||||||
sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
|
sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
|
|
@ -104,7 +106,51 @@ func (wfe *WebFrontEndImpl) NewAuthz(response http.ResponseWriter, request *http
|
||||||
|
|
||||||
body, key, err := verifyPOST(request)
|
body, key, err := verifyPOST(request)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,7 +161,7 @@ func (wfe *WebFrontEndImpl) NewAuthz(response http.ResponseWriter, request *http
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Create new authz and return
|
// Create new authz and return
|
||||||
authz, err := wfe.RA.NewAuthorization(init, key)
|
authz, err := wfe.RA.NewAuthorization(init, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendError(response,
|
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
|
// 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 = ""
|
authz.ID = ""
|
||||||
responseBody, err := json.Marshal(authz)
|
responseBody, err := json.Marshal(authz)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -134,11 +180,12 @@ func (wfe *WebFrontEndImpl) NewAuthz(response http.ResponseWriter, request *http
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Header().Add("Location", authzURL)
|
response.Header().Add("Location", authzURL)
|
||||||
|
response.Header().Add("Link", link(wfe.NewCert, "next"))
|
||||||
response.WriteHeader(http.StatusCreated)
|
response.WriteHeader(http.StatusCreated)
|
||||||
response.Write(responseBody)
|
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" {
|
if request.Method != "POST" {
|
||||||
sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
|
sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
|
|
@ -146,7 +193,7 @@ func (wfe *WebFrontEndImpl) NewCert(response http.ResponseWriter, request *http.
|
||||||
|
|
||||||
body, key, err := verifyPOST(request)
|
body, key, err := verifyPOST(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendError(response, "Unable to read body", http.StatusBadRequest)
|
sendError(response, "Unable to read/verify body", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,7 +214,7 @@ func (wfe *WebFrontEndImpl) NewCert(response http.ResponseWriter, request *http.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make a URL for this authz
|
// Make a URL for this authz
|
||||||
certURL := wfe.certBase + string(cert.ID)
|
certURL := wfe.CertBase + string(cert.ID)
|
||||||
|
|
||||||
// TODO: Content negotiation for cert format
|
// TODO: Content negotiation for cert format
|
||||||
response.Header().Add("Location", certURL)
|
response.Header().Add("Location", certURL)
|
||||||
|
|
@ -175,13 +222,22 @@ func (wfe *WebFrontEndImpl) NewCert(response http.ResponseWriter, request *http.
|
||||||
response.Write(cert.DER)
|
response.Write(cert.DER)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wfe *WebFrontEndImpl) Authz(response http.ResponseWriter, request *http.Request) {
|
func (wfe *WebFrontEndImpl) Challenge(authz core.Authorization, response http.ResponseWriter, request *http.Request) {
|
||||||
// Requests to this handler should have a path that leads to a known authz
|
// Check that the requested challenge exists within the authorization
|
||||||
id := parseIDFromPath(request.URL.Path)
|
found := false
|
||||||
authz, err := wfe.SA.GetAuthorization(id)
|
var challengeIndex int
|
||||||
if err != nil {
|
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,
|
sendError(response,
|
||||||
fmt.Sprintf("Unable to find authorization: %+v", err),
|
fmt.Sprintf("Unable to find challenge"),
|
||||||
http.StatusNotFound)
|
http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -194,12 +250,12 @@ func (wfe *WebFrontEndImpl) Authz(response http.ResponseWriter, request *http.Re
|
||||||
case "POST":
|
case "POST":
|
||||||
body, key, err := verifyPOST(request)
|
body, key, err := verifyPOST(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendError(response, "Unable to read body", http.StatusBadRequest)
|
sendError(response, "Unable to read/verify body", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var initialAuthz core.Authorization
|
var challengeResponse core.Challenge
|
||||||
err = json.Unmarshal(body, &initialAuthz)
|
err = json.Unmarshal(body, &challengeResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendError(response, "Error unmarshaling authorization", http.StatusBadRequest)
|
sendError(response, "Error unmarshaling authorization", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
|
@ -207,15 +263,12 @@ func (wfe *WebFrontEndImpl) Authz(response http.ResponseWriter, request *http.Re
|
||||||
|
|
||||||
// Check that the signing key is the right key
|
// Check that the signing key is the right key
|
||||||
if !key.Equals(authz.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)
|
sendError(response, "Signing key does not match key in authorization", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask the RA to update this authorization
|
// Ask the RA to update this authorization
|
||||||
initialAuthz.ID = authz.ID
|
updatedAuthz, err := wfe.RA.UpdateAuthorization(authz, challengeIndex, challengeResponse)
|
||||||
updatedAuthz, err := wfe.RA.UpdateAuthorization(initialAuthz)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendError(response, "Unable to update authorization", http.StatusInternalServerError)
|
sendError(response, "Unable to update authorization", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
@ -229,6 +282,96 @@ func (wfe *WebFrontEndImpl) Authz(response http.ResponseWriter, request *http.Re
|
||||||
response.WriteHeader(http.StatusAccepted)
|
response.WriteHeader(http.StatusAccepted)
|
||||||
response.Write(jsonReply)
|
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":
|
case "GET":
|
||||||
jsonReply, err := json.Marshal(authz)
|
jsonReply, err := json.Marshal(authz)
|
||||||
if err != nil {
|
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 {
|
switch request.Method {
|
||||||
default:
|
default:
|
||||||
sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
|
sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue