123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389 |
- /*
- Copyright 2016 The Kubernetes Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package app
- import (
- "crypto/ecdsa"
- "crypto/elliptic"
- "crypto/rand"
- "crypto/x509"
- "crypto/x509/pkix"
- "encoding/json"
- "encoding/pem"
- "io/ioutil"
- "math/big"
- "net/http"
- "net/http/httptest"
- "os"
- "path/filepath"
- "sync"
- "testing"
- "time"
- certapi "k8s.io/api/certificates/v1beta1"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/types"
- restclient "k8s.io/client-go/rest"
- certutil "k8s.io/client-go/util/cert"
- capihelper "k8s.io/kubernetes/pkg/apis/certificates/v1beta1"
- "k8s.io/kubernetes/pkg/controller/certificates/authority"
- )
- // Test_buildClientCertificateManager validates that we can build a local client cert
- // manager that will use the bootstrap client until we get a valid cert, then use our
- // provided identity on subsequent requests.
- func Test_buildClientCertificateManager(t *testing.T) {
- testDir, err := ioutil.TempDir("", "kubeletcert")
- if err != nil {
- t.Fatal(err)
- }
- defer func() { os.RemoveAll(testDir) }()
- serverPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
- if err != nil {
- t.Fatal(err)
- }
- serverCA, err := certutil.NewSelfSignedCACert(certutil.Config{
- CommonName: "the-test-framework",
- }, serverPrivateKey)
- if err != nil {
- t.Fatal(err)
- }
- server := &csrSimulator{
- t: t,
- serverPrivateKey: serverPrivateKey,
- serverCA: serverCA,
- }
- s := httptest.NewServer(server)
- defer s.Close()
- config1 := &restclient.Config{
- UserAgent: "FirstClient",
- Host: s.URL,
- }
- config2 := &restclient.Config{
- UserAgent: "SecondClient",
- Host: s.URL,
- }
- nodeName := types.NodeName("test")
- m, err := buildClientCertificateManager(config1, config2, testDir, nodeName)
- if err != nil {
- t.Fatal(err)
- }
- defer m.Stop()
- r := m.(rotater)
- // get an expired CSR (simulating historical output)
- server.backdate = 2 * time.Hour
- server.SetExpectUserAgent("FirstClient")
- ok, err := r.RotateCerts()
- if !ok || err != nil {
- t.Fatalf("unexpected rotation err: %t %v", ok, err)
- }
- if cert := m.Current(); cert != nil {
- t.Fatalf("Unexpected cert, should be expired: %#v", cert)
- }
- fi := getFileInfo(testDir)
- if len(fi) != 2 {
- t.Fatalf("Unexpected directory contents: %#v", fi)
- }
- // if m.Current() == nil, then we try again and get a valid
- // client
- server.backdate = 0
- server.SetExpectUserAgent("FirstClient")
- if ok, err := r.RotateCerts(); !ok || err != nil {
- t.Fatalf("unexpected rotation err: %t %v", ok, err)
- }
- if cert := m.Current(); cert == nil {
- t.Fatalf("Unexpected cert, should be valid: %#v", cert)
- }
- fi = getFileInfo(testDir)
- if len(fi) != 2 {
- t.Fatalf("Unexpected directory contents: %#v", fi)
- }
- // if m.Current() != nil, then we should use the second client
- server.SetExpectUserAgent("SecondClient")
- if ok, err := r.RotateCerts(); !ok || err != nil {
- t.Fatalf("unexpected rotation err: %t %v", ok, err)
- }
- if cert := m.Current(); cert == nil {
- t.Fatalf("Unexpected cert, should be valid: %#v", cert)
- }
- fi = getFileInfo(testDir)
- if len(fi) != 2 {
- t.Fatalf("Unexpected directory contents: %#v", fi)
- }
- }
- func Test_buildClientCertificateManager_populateCertDir(t *testing.T) {
- testDir, err := ioutil.TempDir("", "kubeletcert")
- if err != nil {
- t.Fatal(err)
- }
- defer func() { os.RemoveAll(testDir) }()
- // when no cert is provided, write nothing to disk
- config1 := &restclient.Config{
- UserAgent: "FirstClient",
- Host: "http://localhost",
- }
- config2 := &restclient.Config{
- UserAgent: "SecondClient",
- Host: "http://localhost",
- }
- nodeName := types.NodeName("test")
- if _, err := buildClientCertificateManager(config1, config2, testDir, nodeName); err != nil {
- t.Fatal(err)
- }
- fi := getFileInfo(testDir)
- if len(fi) != 0 {
- t.Fatalf("Unexpected directory contents: %#v", fi)
- }
- // an invalid cert should be ignored
- config2.CertData = []byte("invalid contents")
- config2.KeyData = []byte("invalid contents")
- if _, err := buildClientCertificateManager(config1, config2, testDir, nodeName); err == nil {
- t.Fatal("unexpected non error")
- }
- fi = getFileInfo(testDir)
- if len(fi) != 0 {
- t.Fatalf("Unexpected directory contents: %#v", fi)
- }
- // an expired client certificate should be written to disk, because the cert manager can
- // use config1 to refresh it and the cert manager won't return it for clients.
- config2.CertData, config2.KeyData = genClientCert(t, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
- if _, err := buildClientCertificateManager(config1, config2, testDir, nodeName); err != nil {
- t.Fatal(err)
- }
- fi = getFileInfo(testDir)
- if len(fi) != 2 {
- t.Fatalf("Unexpected directory contents: %#v", fi)
- }
- // a valid, non-expired client certificate should be written to disk
- config2.CertData, config2.KeyData = genClientCert(t, time.Now().Add(-time.Hour), time.Now().Add(24*time.Hour))
- if _, err := buildClientCertificateManager(config1, config2, testDir, nodeName); err != nil {
- t.Fatal(err)
- }
- fi = getFileInfo(testDir)
- if len(fi) != 2 {
- t.Fatalf("Unexpected directory contents: %#v", fi)
- }
- }
- func getFileInfo(dir string) map[string]os.FileInfo {
- fi := make(map[string]os.FileInfo)
- filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
- if path == dir {
- return nil
- }
- fi[path] = info
- if !info.IsDir() {
- os.Remove(path)
- }
- return nil
- })
- return fi
- }
- type rotater interface {
- RotateCerts() (bool, error)
- }
- func getCSR(req *http.Request) (*certapi.CertificateSigningRequest, error) {
- if req.Body == nil {
- return nil, nil
- }
- body, err := ioutil.ReadAll(req.Body)
- if err != nil {
- return nil, err
- }
- csr := &certapi.CertificateSigningRequest{}
- if err := json.Unmarshal(body, csr); err != nil {
- return nil, err
- }
- return csr, nil
- }
- func mustMarshal(obj interface{}) []byte {
- data, err := json.Marshal(obj)
- if err != nil {
- panic(err)
- }
- return data
- }
- type csrSimulator struct {
- t *testing.T
- serverPrivateKey *ecdsa.PrivateKey
- serverCA *x509.Certificate
- backdate time.Duration
- userAgentLock sync.Mutex
- expectUserAgent string
- lock sync.Mutex
- csr *certapi.CertificateSigningRequest
- }
- func (s *csrSimulator) SetExpectUserAgent(a string) {
- s.userAgentLock.Lock()
- defer s.userAgentLock.Unlock()
- s.expectUserAgent = a
- }
- func (s *csrSimulator) ExpectUserAgent() string {
- s.userAgentLock.Lock()
- defer s.userAgentLock.Unlock()
- return s.expectUserAgent
- }
- func (s *csrSimulator) ServeHTTP(w http.ResponseWriter, req *http.Request) {
- s.lock.Lock()
- defer s.lock.Unlock()
- t := s.t
- // filter out timeouts as csrSimulator don't support them
- q := req.URL.Query()
- q.Del("timeout")
- q.Del("timeoutSeconds")
- q.Del("allowWatchBookmarks")
- req.URL.RawQuery = q.Encode()
- t.Logf("Request %q %q %q", req.Method, req.URL, req.UserAgent())
- if a := s.ExpectUserAgent(); len(a) > 0 && req.UserAgent() != a {
- t.Errorf("Unexpected user agent: %s", req.UserAgent())
- }
- switch {
- case req.Method == "POST" && req.URL.Path == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests":
- csr, err := getCSR(req)
- if err != nil {
- t.Fatal(err)
- }
- if csr.Name == "" {
- csr.Name = "test-csr"
- }
- csr.UID = types.UID("1")
- csr.ResourceVersion = "1"
- data := mustMarshal(csr)
- w.Header().Set("Content-Type", "application/json")
- w.Write(data)
- csr = csr.DeepCopy()
- csr.ResourceVersion = "2"
- ca := &authority.CertificateAuthority{
- Certificate: s.serverCA,
- PrivateKey: s.serverPrivateKey,
- Backdate: s.backdate,
- }
- cr, err := capihelper.ParseCSR(csr)
- if err != nil {
- t.Fatal(err)
- }
- der, err := ca.Sign(cr.Raw, authority.PermissiveSigningPolicy{
- TTL: time.Hour,
- })
- if err != nil {
- t.Fatal(err)
- }
- csr.Status.Certificate = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
- csr.Status.Conditions = []certapi.CertificateSigningRequestCondition{
- {Type: certapi.CertificateApproved},
- }
- s.csr = csr
- case req.Method == "GET" && req.URL.Path == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests" && req.URL.RawQuery == "fieldSelector=metadata.name%3Dtest-csr&limit=500&resourceVersion=0":
- if s.csr == nil {
- t.Fatalf("no csr")
- }
- csr := s.csr.DeepCopy()
- data := mustMarshal(&certapi.CertificateSigningRequestList{
- ListMeta: metav1.ListMeta{
- ResourceVersion: "2",
- },
- Items: []certapi.CertificateSigningRequest{
- *csr,
- },
- })
- w.Header().Set("Content-Type", "application/json")
- w.Write(data)
- case req.Method == "GET" && req.URL.Path == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests" && req.URL.RawQuery == "fieldSelector=metadata.name%3Dtest-csr&resourceVersion=2&watch=true":
- if s.csr == nil {
- t.Fatalf("no csr")
- }
- csr := s.csr.DeepCopy()
- data := mustMarshal(&metav1.WatchEvent{
- Type: "ADDED",
- Object: runtime.RawExtension{
- Raw: mustMarshal(csr),
- },
- })
- w.Header().Set("Content-Type", "application/json")
- w.Write(data)
- default:
- t.Fatalf("unexpected request: %s %s", req.Method, req.URL)
- }
- }
- // genClientCert generates an x509 certificate for testing. Certificate and key
- // are returned in PEM encoding.
- func genClientCert(t *testing.T, from, to time.Time) ([]byte, []byte) {
- key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
- if err != nil {
- t.Fatal(err)
- }
- keyRaw, err := x509.MarshalECPrivateKey(key)
- if err != nil {
- t.Fatal(err)
- }
- serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
- serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
- if err != nil {
- t.Fatal(err)
- }
- cert := &x509.Certificate{
- SerialNumber: serialNumber,
- Subject: pkix.Name{Organization: []string{"Acme Co"}},
- NotBefore: from,
- NotAfter: to,
- KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
- ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
- BasicConstraintsValid: true,
- }
- certRaw, err := x509.CreateCertificate(rand.Reader, cert, cert, key.Public(), key)
- if err != nil {
- t.Fatal(err)
- }
- return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certRaw}),
- pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyRaw})
- }
|