diff --git a/ca/bootstrap_test.go b/ca/bootstrap_test.go index 241827c6..00bef552 100644 --- a/ca/bootstrap_test.go +++ b/ca/bootstrap_test.go @@ -3,12 +3,15 @@ package ca import ( "context" "crypto/tls" + "fmt" + "net" "net/http" "net/http/httptest" "reflect" "testing" "time" + "github.com/pkg/errors" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority" @@ -18,6 +21,24 @@ import ( "gopkg.in/square/go-jose.v2/jwt" ) +func newLocalListener() net.Listener { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + if l, err = net.Listen("tcp6", "[::1]:0"); err != nil { + panic(fmt.Sprintf("failed to listen on a port: %v", err)) + } + } + return l +} + +func setMinCertDuration(d time.Duration) func() { + tmp := minCertDuration + minCertDuration = 1 * time.Second + return func() { + minCertDuration = tmp + } +} + func startCABootstrapServer() *httptest.Server { config, err := authority.LoadConfiguration("testdata/ca.json") if err != nil { @@ -267,3 +288,112 @@ func TestBootstrapClient(t *testing.T) { }) } } + +func TestBootstrapClientRotation(t *testing.T) { + reset := setMinCertDuration(1 * time.Second) + defer reset() + + // Configuration with current root + config, err := authority.LoadConfiguration("testdata/rotate-ca-0.json") + if err != nil { + panic(err) + } + + // Get local address + listener := newLocalListener() + config.Address = listener.Addr().String() + srvURL := "https://" + listener.Addr().String() + + // Start CA server + ca, err := New(config) + if err != nil { + panic(err) + } + go func() { + ca.srv.Serve(listener) + }() + defer ca.Stop() + time.Sleep(1 * time.Second) + + // doTest does a request that requires mTLS + doTest := func(client *http.Client) error { + resp, err := client.Get(srvURL + "/roots") + if err != nil { + return errors.New("client.Get() failed getting roots") + } + var roots api.RootsResponse + if err := readJSON(resp.Body, &roots); err != nil { + return errors.Errorf("client.Get() error reading response: %v", err) + } + return nil + } + + // Create bootstrap client + token := generateBootstrapToken(srvURL, "subject", "ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7") + client, err := BootstrapClient(context.Background(), token) + if err != nil { + t.Errorf("BootstrapClient() error = %v", err) + return + } + + // Test with default root + if err := doTest(client); err != nil { + t.Errorf("Test with rotate-ca-0.json failed: %v", err) + } + + // wait for renew + time.Sleep(5 * time.Second) + + // Reload with configuration with current and future root + ca.opts.configFile = "testdata/rotate-ca-1.json" + if err := doReload(ca); err != nil { + t.Errorf("ca.Reload() error = %v", err) + return + } + if err := doTest(client); err != nil { + t.Errorf("Test with rotate-ca-1.json failed: %v", err) + } + + // wait for renew + time.Sleep(5 * time.Second) + + // Reload with new and old root + ca.opts.configFile = "testdata/rotate-ca-2.json" + if err := doReload(ca); err != nil { + t.Errorf("ca.Reload() error = %v", err) + return + } + if err := doTest(client); err != nil { + t.Errorf("Test with rotate-ca-2.json failed: %v", err) + } + + // wait for renew + time.Sleep(5 * time.Second) + + // Reload with pnly the new root + ca.opts.configFile = "testdata/rotate-ca-3.json" + if err := doReload(ca); err != nil { + t.Errorf("ca.Reload() error = %v", err) + return + } + if err := doTest(client); err != nil { + t.Errorf("Test with rotate-ca-3.json failed: %v", err) + } +} + +// doReload uses the reload implementation but overwrites the new address with +// the one being used. +func doReload(ca *CA) error { + config, err := authority.LoadConfiguration(ca.opts.configFile) + if err != nil { + return errors.Wrap(err, "error reloading ca") + } + + newCA, err := New(config, WithPassword(ca.opts.password), WithConfigFile(ca.opts.configFile)) + if err != nil { + return errors.Wrap(err, "error reloading ca") + } + // Use same address in new server + newCA.srv.Addr = ca.srv.Addr + return ca.srv.Reload(newCA.srv) +} diff --git a/ca/testdata/ca.json b/ca/testdata/ca.json index f5484a7c..f29f24c6 100644 --- a/ca/testdata/ca.json +++ b/ca/testdata/ca.json @@ -18,7 +18,6 @@ ] }, "authority": { - "minCertDuration": "1m", "provisioners": [ { "name": "max", @@ -73,7 +72,7 @@ "y": "e3wycXwVB366F0wLE5J9gIpq8EIQ4900nHBNpIGebEA" }, "claims": { - "minTLSCertDuration": "30s" + "minTLSCertDuration": "1s" } }, { "name": "mariano", diff --git a/ca/testdata/rotate-ca-0.json b/ca/testdata/rotate-ca-0.json new file mode 100644 index 00000000..20dd603a --- /dev/null +++ b/ca/testdata/rotate-ca-0.json @@ -0,0 +1,46 @@ +{ + "root": "testdata/secrets/root_ca.crt", + "crt": "testdata/secrets/intermediate_ca.crt", + "key": "testdata/secrets/intermediate_ca_key", + "password": "password", + "address": "127.0.0.1:0", + "dnsNames": ["127.0.0.1"], + "logger": {"format": "text"}, + "tls": { + "minVersion": 1.2, + "maxVersion": 1.2, + "renegotiation": false, + "cipherSuites": [ + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" + ] + }, + "authority": { + "provisioners": [ + { + "name": "mariano", + "type": "jwk", + "encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlB1UnJVQ1RZZkR1T2F5MEh2cGl6bncifQ.7a-OP5xWGbFra8m2MN9YuLGt6v4y0wmB.u-54daK2y-0UO9na.3GQy6E52-fOSUu5NJ_sEbxj_T3CTyWb7wOPFv2oI2PBWXp5CLpiWJbCFpF4v2oD9fN5XbxMP14ootbrFjATnoMWfWgyLwG-KOj9BqMGNxhG2v37yC7Wrris6s30nrPa3uyNEYZ12AOQW1K04cU2X0u_qJM3vzMCle548ZFTWs6_d6L8lp3o0F9MEbCmJ4p6CLqQxjxYtn1aD79lM91NbIXpRP3iUFQRly-y_iC2mSkXCdd_cQ6-dqLUchXwWRyVO5nBHb4J87aZ91VApw7ldTLtwRZ2ZGJpqGQGgjTwi4sgjEcMuGg0_83XGk2ubdlKDpmGFedOHS5rYCbxotts.vSYfxsi2UU9LQeySDjAnnQ", + "key": { + "use": "sig", + "kty": "EC", + "kid": "FLIV7q23CXHrg75J2OSbvzwKJJqoxCYixjmsJirneOg", + "crv": "P-256", + "alg": "ES256", + "x": "tTKthEHN7RuybhkaC43J2oLfBG995FNSWbtahLAiK7Y", + "y": "e3wycXwVB366F0wLE5J9gIpq8EIQ4900nHBNpIGebEA" + }, + "claims": { + "minTLSCertDuration": "1s", + "defaultTLSCertDuration": "5s" + } + } + ], + "template": { + "country": "US", + "locality": "San Francisco", + "organization": "Smallstep" + } + } +} diff --git a/ca/testdata/rotate-ca-1.json b/ca/testdata/rotate-ca-1.json new file mode 100644 index 00000000..b038f694 --- /dev/null +++ b/ca/testdata/rotate-ca-1.json @@ -0,0 +1,46 @@ +{ + "root": ["testdata/secrets/root_ca.crt", "testdata/rotated/root_ca.crt"], + "crt": "testdata/secrets/intermediate_ca.crt", + "key": "testdata/secrets/intermediate_ca_key", + "password": "password", + "address": "127.0.0.1:0", + "dnsNames": ["127.0.0.1"], + "logger": {"format": "text"}, + "tls": { + "minVersion": 1.2, + "maxVersion": 1.2, + "renegotiation": false, + "cipherSuites": [ + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" + ] + }, + "authority": { + "provisioners": [ + { + "name": "mariano", + "type": "jwk", + "encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlB1UnJVQ1RZZkR1T2F5MEh2cGl6bncifQ.7a-OP5xWGbFra8m2MN9YuLGt6v4y0wmB.u-54daK2y-0UO9na.3GQy6E52-fOSUu5NJ_sEbxj_T3CTyWb7wOPFv2oI2PBWXp5CLpiWJbCFpF4v2oD9fN5XbxMP14ootbrFjATnoMWfWgyLwG-KOj9BqMGNxhG2v37yC7Wrris6s30nrPa3uyNEYZ12AOQW1K04cU2X0u_qJM3vzMCle548ZFTWs6_d6L8lp3o0F9MEbCmJ4p6CLqQxjxYtn1aD79lM91NbIXpRP3iUFQRly-y_iC2mSkXCdd_cQ6-dqLUchXwWRyVO5nBHb4J87aZ91VApw7ldTLtwRZ2ZGJpqGQGgjTwi4sgjEcMuGg0_83XGk2ubdlKDpmGFedOHS5rYCbxotts.vSYfxsi2UU9LQeySDjAnnQ", + "key": { + "use": "sig", + "kty": "EC", + "kid": "FLIV7q23CXHrg75J2OSbvzwKJJqoxCYixjmsJirneOg", + "crv": "P-256", + "alg": "ES256", + "x": "tTKthEHN7RuybhkaC43J2oLfBG995FNSWbtahLAiK7Y", + "y": "e3wycXwVB366F0wLE5J9gIpq8EIQ4900nHBNpIGebEA" + }, + "claims": { + "minTLSCertDuration": "1s", + "defaultTLSCertDuration": "5s" + } + } + ], + "template": { + "country": "US", + "locality": "San Francisco", + "organization": "Smallstep" + } + } +} diff --git a/ca/testdata/rotate-ca-2.json b/ca/testdata/rotate-ca-2.json new file mode 100644 index 00000000..7ec965d0 --- /dev/null +++ b/ca/testdata/rotate-ca-2.json @@ -0,0 +1,46 @@ +{ + "root": ["testdata/rotated/root_ca.crt", "testdata/secrets/root_ca.crt"], + "crt": "testdata/rotated/intermediate_ca.crt", + "key": "testdata/rotated/intermediate_ca_key", + "password": "asdf", + "address": "127.0.0.1:0", + "dnsNames": ["127.0.0.1"], + "logger": {"format": "text"}, + "tls": { + "minVersion": 1.2, + "maxVersion": 1.2, + "renegotiation": false, + "cipherSuites": [ + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" + ] + }, + "authority": { + "provisioners": [ + { + "name": "mariano", + "type": "jwk", + "encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlB1UnJVQ1RZZkR1T2F5MEh2cGl6bncifQ.7a-OP5xWGbFra8m2MN9YuLGt6v4y0wmB.u-54daK2y-0UO9na.3GQy6E52-fOSUu5NJ_sEbxj_T3CTyWb7wOPFv2oI2PBWXp5CLpiWJbCFpF4v2oD9fN5XbxMP14ootbrFjATnoMWfWgyLwG-KOj9BqMGNxhG2v37yC7Wrris6s30nrPa3uyNEYZ12AOQW1K04cU2X0u_qJM3vzMCle548ZFTWs6_d6L8lp3o0F9MEbCmJ4p6CLqQxjxYtn1aD79lM91NbIXpRP3iUFQRly-y_iC2mSkXCdd_cQ6-dqLUchXwWRyVO5nBHb4J87aZ91VApw7ldTLtwRZ2ZGJpqGQGgjTwi4sgjEcMuGg0_83XGk2ubdlKDpmGFedOHS5rYCbxotts.vSYfxsi2UU9LQeySDjAnnQ", + "key": { + "use": "sig", + "kty": "EC", + "kid": "FLIV7q23CXHrg75J2OSbvzwKJJqoxCYixjmsJirneOg", + "crv": "P-256", + "alg": "ES256", + "x": "tTKthEHN7RuybhkaC43J2oLfBG995FNSWbtahLAiK7Y", + "y": "e3wycXwVB366F0wLE5J9gIpq8EIQ4900nHBNpIGebEA" + }, + "claims": { + "minTLSCertDuration": "1s", + "defaultTLSCertDuration": "5s" + } + } + ], + "template": { + "country": "US", + "locality": "San Francisco", + "organization": "Smallstep" + } + } +} diff --git a/ca/testdata/rotate-ca-3.json b/ca/testdata/rotate-ca-3.json new file mode 100644 index 00000000..968da6ba --- /dev/null +++ b/ca/testdata/rotate-ca-3.json @@ -0,0 +1,46 @@ +{ + "root": "testdata/rotated/root_ca.crt", + "crt": "testdata/rotated/intermediate_ca.crt", + "key": "testdata/rotated/intermediate_ca_key", + "password": "asdf", + "address": "127.0.0.1:0", + "dnsNames": ["127.0.0.1"], + "logger": {"format": "text"}, + "tls": { + "minVersion": 1.2, + "maxVersion": 1.2, + "renegotiation": false, + "cipherSuites": [ + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" + ] + }, + "authority": { + "provisioners": [ + { + "name": "mariano", + "type": "jwk", + "encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlB1UnJVQ1RZZkR1T2F5MEh2cGl6bncifQ.7a-OP5xWGbFra8m2MN9YuLGt6v4y0wmB.u-54daK2y-0UO9na.3GQy6E52-fOSUu5NJ_sEbxj_T3CTyWb7wOPFv2oI2PBWXp5CLpiWJbCFpF4v2oD9fN5XbxMP14ootbrFjATnoMWfWgyLwG-KOj9BqMGNxhG2v37yC7Wrris6s30nrPa3uyNEYZ12AOQW1K04cU2X0u_qJM3vzMCle548ZFTWs6_d6L8lp3o0F9MEbCmJ4p6CLqQxjxYtn1aD79lM91NbIXpRP3iUFQRly-y_iC2mSkXCdd_cQ6-dqLUchXwWRyVO5nBHb4J87aZ91VApw7ldTLtwRZ2ZGJpqGQGgjTwi4sgjEcMuGg0_83XGk2ubdlKDpmGFedOHS5rYCbxotts.vSYfxsi2UU9LQeySDjAnnQ", + "key": { + "use": "sig", + "kty": "EC", + "kid": "FLIV7q23CXHrg75J2OSbvzwKJJqoxCYixjmsJirneOg", + "crv": "P-256", + "alg": "ES256", + "x": "tTKthEHN7RuybhkaC43J2oLfBG995FNSWbtahLAiK7Y", + "y": "e3wycXwVB366F0wLE5J9gIpq8EIQ4900nHBNpIGebEA" + }, + "claims": { + "minTLSCertDuration": "1s", + "defaultTLSCertDuration": "5s" + } + } + ], + "template": { + "country": "US", + "locality": "San Francisco", + "organization": "Smallstep" + } + } +} diff --git a/ca/testdata/rotated/intermediate_ca.crt b/ca/testdata/rotated/intermediate_ca.crt new file mode 100644 index 00000000..338ebb22 --- /dev/null +++ b/ca/testdata/rotated/intermediate_ca.crt @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBxTCCAWugAwIBAgIQLIY6MR/1fBRQY4ZTTsPAJjAKBggqhkjOPQQDAjAcMRow +GAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xOTAxMDcyMDExMzBaFw0yOTAx +MDQyMDExMzBaMCQxIjAgBgNVBAMTGVNtYWxsc3RlcCBJbnRlcm1lZGlhdGUgQ0Ew +WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARgtjL/KLNpdq81YYWaek1lrkPM/QF1 +m+ujwv5jya21fAXljdBLh6m2xco1GPfwPBbwUGlNOdEqE9Nq3Qx3ngPKo4GGMIGD +MA4GA1UdDwEB/wQEAwIBpjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw +EgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUqixeZ/K1HW9N6SVw7ONya98S +u8UwHwYDVR0jBBgwFoAUgIzlCLxh/RlwEany4JQHOorLAIEwCgYIKoZIzj0EAwID +SAAwRQIgdGX6lxThrKlt3v+3HJZlaWdmoeQ3vYwpJb9uHExZdVYCIQDCxsdI8EnB +bxjnJscbT4zvqVsq6AmycdbFwgy8RIeVzg== +-----END CERTIFICATE----- diff --git a/ca/testdata/rotated/intermediate_ca_key b/ca/testdata/rotated/intermediate_ca_key new file mode 100644 index 00000000..6c3b1622 --- /dev/null +++ b/ca/testdata/rotated/intermediate_ca_key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,7dcc0a8c1d73c8d438184e0928875329 + +r6yrQrHg6zBZRSjQpe8RzyQALEfiT3/8lMvvPu3BX6yign5skMfCVMXZhzbmAwmR +BJBIX+5hkudR2VN+hrsOyuU7FvIk4gx2c8buIlFObfYXIml0mpuThfm52ciAtOTE +S0hkfYvPcOAjzaDZ+8Po/mYhkODgyvijogn4ioTF/Ss= +-----END EC PRIVATE KEY----- diff --git a/ca/testdata/rotated/root_ca.crt b/ca/testdata/rotated/root_ca.crt new file mode 100644 index 00000000..87cd5650 --- /dev/null +++ b/ca/testdata/rotated/root_ca.crt @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBfTCCASKgAwIBAgIRAJPUE0MTA+fMz6f6i/XYmTwwCgYIKoZIzj0EAwIwHDEa +MBgGA1UEAxMRU21hbGxzdGVwIFJvb3QgQ0EwHhcNMTkwMTA3MjAxMTMwWhcNMjkw +MTA0MjAxMTMwWjAcMRowGAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTBZMBMGByqG +SM49AgEGCCqGSM49AwEHA0IABCOH/PGThn0cMOGDeqDxb22olsdCm8hVdyW9cHQL +jfIYAqpWNh9f7E5umlnxkOy6OEROTtpq7etzfBbzb52loVWjRTBDMA4GA1UdDwEB +/wQEAwIBpjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBSAjOUIvGH9GXAR +qfLglAc6issAgTAKBggqhkjOPQQDAgNJADBGAiEAjs0yjbQ/9dmGoUn7JS3lE83z +YlnXZ0fHdeNakkIKhQICIQCUENhGZp63pMtm3ipgwp91EM0T7YtKgrFNvDekqufc +Sw== +-----END CERTIFICATE----- diff --git a/ca/testdata/rotated/root_ca_key b/ca/testdata/rotated/root_ca_key new file mode 100644 index 00000000..c92f587e --- /dev/null +++ b/ca/testdata/rotated/root_ca_key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,8ce79d28601b9809905ef7c362a20749 + +H+pTTL3B5fLYycgHLxFOW0fZsayr7Y+BW8THKf12h8dk0/eOE1wNoX2TuMtpbZgO +lMJdFPL+SAPCCmuZOZIcQDejRHVcYBq1wvrrnw/yfVawXC4xze+J4Y+q0J2WY+rM +xcLGlEOIRZkvdDVGmSitEZBl0Ibk0p9tG++7QGqAvnk= +-----END EC PRIVATE KEY-----