token_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. /*
  2. Copyright 2017 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package token
  14. import (
  15. "testing"
  16. "time"
  17. v1 "k8s.io/api/core/v1"
  18. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  19. clientset "k8s.io/client-go/kubernetes"
  20. fakeclient "k8s.io/client-go/kubernetes/fake"
  21. "k8s.io/client-go/tools/clientcmd"
  22. bootstrapapi "k8s.io/cluster-bootstrap/token/api"
  23. tokenjws "k8s.io/cluster-bootstrap/token/jws"
  24. kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
  25. "k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient"
  26. "github.com/pmezard/go-difflib/difflib"
  27. )
  28. func TestRetrieveValidatedConfigInfo(t *testing.T) {
  29. const (
  30. caCert = `-----BEGIN CERTIFICATE-----
  31. MIICyDCCAbCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl
  32. cm5ldGVzMB4XDTE5MTEyMDAwNDk0MloXDTI5MTExNzAwNDk0MlowFTETMBEGA1UE
  33. AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMqQ
  34. ctECzA8yFSuVYupOUYgrTmfQeKe/9BaDWagaq7ow9+I2IvsfWFvlrD8QQr8sea6q
  35. xjq7TV67Vb4RxBaoYDA+yI5vIcujWUxULun64lu3Q6iC1sj2UnmUpIdgazRXXEkZ
  36. vxA6EbAnoxA0+lBOn1CZWl23IQ4s70o2hZ7wIp/vevB88RRRjqtvgc5elsjsbmDF
  37. LS7L1Zuye8c6gS93bR+VjVmSIfr1IEq0748tIIyXjAVCWPVCvuP41MlfPc/JVpZD
  38. uD2+pO6ZYREcdAnOf2eD4/eLOMKko4L1dSFy9JKM5PLnOC0Zk0AYOd1vS8DTAfxj
  39. XPEIY8OBYFhlsxf4TE8CAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB
  40. /wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH/OYq8zyl1+zSTmuow3yI/15PL1
  41. dl8hB7IKnZNWmC/LTdm/+noh3Sb1IdRv6HkKg/GUn0UMuRUngLhju3EO4ozJPQcX
  42. quaxzgmTKNWJ6ErDvRvWhGX0ZcbdBfZv+dowyRqzd5nlJ49hC+NrtFFQq6P05BYn
  43. 7SemguqeXmXwIj2Sa+1DeR6lRm9o8shAYjnyThUFqaMn18kI3SANJ5vk/3DFrPEO
  44. CKC9EzFku2kuxg2dM12PbRGZQ2o0K6HEZgrrIKTPOy3ocb8r9M0aSFhjOV/NqGA4
  45. SaupXSW6XfvIi/UHoIbU3pNcsnUJGnQfQvip95XKk/gqcUr+m50vxgumxtA=
  46. -----END CERTIFICATE-----`
  47. caCertHash = "sha256:98be2e6d4d8a89aa308fb15de0c07e2531ce549c68dec1687cdd5c06f0826658"
  48. expectedKubeconfig = `apiVersion: v1
  49. clusters:
  50. - cluster:
  51. certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRFNU1URXlNREF3TkRrME1sb1hEVEk1TVRFeE56QXdORGswTWxvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTXFRCmN0RUN6QTh5RlN1Vll1cE9VWWdyVG1mUWVLZS85QmFEV2FnYXE3b3c5K0kySXZzZldGdmxyRDhRUXI4c2VhNnEKeGpxN1RWNjdWYjRSeEJhb1lEQSt5STV2SWN1aldVeFVMdW42NGx1M1E2aUMxc2oyVW5tVXBJZGdhelJYWEVrWgp2eEE2RWJBbm94QTArbEJPbjFDWldsMjNJUTRzNzBvMmhaN3dJcC92ZXZCODhSUlJqcXR2Z2M1ZWxzanNibURGCkxTN0wxWnV5ZThjNmdTOTNiUitWalZtU0lmcjFJRXEwNzQ4dElJeVhqQVZDV1BWQ3Z1UDQxTWxmUGMvSlZwWkQKdUQyK3BPNlpZUkVjZEFuT2YyZUQ0L2VMT01La280TDFkU0Z5OUpLTTVQTG5PQzBaazBBWU9kMXZTOERUQWZ4agpYUEVJWThPQllGaGxzeGY0VEU4Q0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFIL09ZcTh6eWwxK3pTVG11b3czeUkvMTVQTDEKZGw4aEI3SUtuWk5XbUMvTFRkbS8rbm9oM1NiMUlkUnY2SGtLZy9HVW4wVU11UlVuZ0xoanUzRU80b3pKUFFjWApxdWF4emdtVEtOV0o2RXJEdlJ2V2hHWDBaY2JkQmZaditkb3d5UnF6ZDVubEo0OWhDK05ydEZGUXE2UDA1QlluCjdTZW1ndXFlWG1Yd0lqMlNhKzFEZVI2bFJtOW84c2hBWWpueVRoVUZxYU1uMThrSTNTQU5KNXZrLzNERnJQRU8KQ0tDOUV6Rmt1Mmt1eGcyZE0xMlBiUkdaUTJvMEs2SEVaZ3JySUtUUE95M29jYjhyOU0wYVNGaGpPVi9OcUdBNApTYXVwWFNXNlhmdklpL1VIb0liVTNwTmNzblVKR25RZlF2aXA5NVhLay9ncWNVcittNTB2eGd1bXh0QT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
  52. server: https://127.0.0.1
  53. name: somecluster
  54. contexts:
  55. - context:
  56. cluster: somecluster
  57. user: token-bootstrap-client
  58. name: token-bootstrap-client@somecluster
  59. current-context: token-bootstrap-client@somecluster
  60. kind: Config
  61. preferences: {}
  62. users: null
  63. `
  64. )
  65. tests := []struct {
  66. name string
  67. tokenID string
  68. tokenSecret string
  69. cfg *kubeadmapi.Discovery
  70. configMap *fakeConfigMap
  71. delayedJWSSignaturePatch bool
  72. expectedError bool
  73. }{
  74. {
  75. // This is the default behavior. The JWS signature is patched after the cluster-info ConfigMap is created
  76. name: "valid: retrieve a valid kubeconfig with CA verification and delayed JWS signature",
  77. tokenID: "123456",
  78. tokenSecret: "abcdef1234567890",
  79. cfg: &kubeadmapi.Discovery{
  80. BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{
  81. Token: "123456.abcdef1234567890",
  82. CACertHashes: []string{caCertHash},
  83. },
  84. },
  85. configMap: &fakeConfigMap{
  86. name: bootstrapapi.ConfigMapClusterInfo,
  87. data: map[string]string{},
  88. },
  89. delayedJWSSignaturePatch: true,
  90. },
  91. {
  92. // Same as above expect this test creates the ConfigMap with the JWS signature
  93. name: "valid: retrieve a valid kubeconfig with CA verification",
  94. tokenID: "123456",
  95. tokenSecret: "abcdef1234567890",
  96. cfg: &kubeadmapi.Discovery{
  97. BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{
  98. Token: "123456.abcdef1234567890",
  99. CACertHashes: []string{caCertHash},
  100. },
  101. },
  102. configMap: &fakeConfigMap{
  103. name: bootstrapapi.ConfigMapClusterInfo,
  104. data: nil,
  105. },
  106. },
  107. {
  108. // Skipping CA verification is also supported
  109. name: "valid: retrieve a valid kubeconfig without CA verification",
  110. tokenID: "123456",
  111. tokenSecret: "abcdef1234567890",
  112. cfg: &kubeadmapi.Discovery{
  113. BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{
  114. Token: "123456.abcdef1234567890",
  115. },
  116. },
  117. configMap: &fakeConfigMap{
  118. name: bootstrapapi.ConfigMapClusterInfo,
  119. data: nil,
  120. },
  121. },
  122. {
  123. name: "invalid: token format is invalid",
  124. tokenID: "foo",
  125. tokenSecret: "bar",
  126. cfg: &kubeadmapi.Discovery{
  127. BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{
  128. Token: "foo.bar",
  129. },
  130. },
  131. configMap: &fakeConfigMap{
  132. name: bootstrapapi.ConfigMapClusterInfo,
  133. data: nil,
  134. },
  135. expectedError: true,
  136. },
  137. {
  138. name: "invalid: missing cluster-info ConfigMap",
  139. tokenID: "123456",
  140. tokenSecret: "abcdef1234567890",
  141. cfg: &kubeadmapi.Discovery{
  142. BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{
  143. Token: "123456.abcdef1234567890",
  144. },
  145. },
  146. configMap: &fakeConfigMap{
  147. name: "baz",
  148. data: nil,
  149. },
  150. expectedError: true,
  151. },
  152. {
  153. name: "invalid: wrong JWS signature",
  154. tokenID: "123456",
  155. tokenSecret: "abcdef1234567890",
  156. cfg: &kubeadmapi.Discovery{
  157. BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{
  158. Token: "123456.abcdef1234567890",
  159. },
  160. },
  161. configMap: &fakeConfigMap{
  162. name: bootstrapapi.ConfigMapClusterInfo,
  163. data: map[string]string{
  164. bootstrapapi.KubeConfigKey: "foo",
  165. bootstrapapi.JWSSignatureKeyPrefix + "123456": "bar",
  166. },
  167. },
  168. expectedError: true,
  169. },
  170. {
  171. name: "invalid: missing key for JWSSignatureKeyPrefix",
  172. tokenID: "123456",
  173. tokenSecret: "abcdef1234567890",
  174. cfg: &kubeadmapi.Discovery{
  175. BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{
  176. Token: "123456.abcdef1234567890",
  177. },
  178. },
  179. configMap: &fakeConfigMap{
  180. name: bootstrapapi.ConfigMapClusterInfo,
  181. data: map[string]string{
  182. bootstrapapi.KubeConfigKey: "foo",
  183. },
  184. },
  185. expectedError: true,
  186. },
  187. {
  188. name: "invalid: wrong CA cert hash",
  189. tokenID: "123456",
  190. tokenSecret: "abcdef1234567890",
  191. cfg: &kubeadmapi.Discovery{
  192. BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{
  193. Token: "123456.abcdef1234567890",
  194. CACertHashes: []string{"foo"},
  195. },
  196. },
  197. configMap: &fakeConfigMap{
  198. name: bootstrapapi.ConfigMapClusterInfo,
  199. data: nil,
  200. },
  201. expectedError: true,
  202. },
  203. }
  204. for _, test := range tests {
  205. t.Run(test.name, func(t *testing.T) {
  206. kubeconfig := buildSecureBootstrapKubeConfig("127.0.0.1", []byte(caCert), "somecluster")
  207. kubeconfigBytes, err := clientcmd.Write(*kubeconfig)
  208. if err != nil {
  209. t.Fatalf("cannot marshal kubeconfig %v", err)
  210. }
  211. // Generate signature of the insecure kubeconfig
  212. sig, err := tokenjws.ComputeDetachedSignature(string(kubeconfigBytes), test.tokenID, test.tokenSecret)
  213. if err != nil {
  214. t.Fatalf("cannot compute detached JWS signature: %v", err)
  215. }
  216. // If the JWS signature is delayed, only add the kubeconfig
  217. if test.delayedJWSSignaturePatch {
  218. test.configMap.data = map[string]string{}
  219. test.configMap.data[bootstrapapi.KubeConfigKey] = string(kubeconfigBytes)
  220. }
  221. // Populate the default cluster-info data
  222. if test.configMap.data == nil {
  223. test.configMap.data = map[string]string{}
  224. test.configMap.data[bootstrapapi.KubeConfigKey] = string(kubeconfigBytes)
  225. test.configMap.data[bootstrapapi.JWSSignatureKeyPrefix+test.tokenID] = sig
  226. }
  227. // Create a fake client and create the cluster-info ConfigMap
  228. client := fakeclient.NewSimpleClientset()
  229. if err = test.configMap.createOrUpdate(client); err != nil {
  230. t.Fatalf("could not create ConfigMap: %v", err)
  231. }
  232. // Set arbitrary discovery timeout and retry interval
  233. test.cfg.Timeout = &metav1.Duration{Duration: time.Millisecond * 200}
  234. interval := time.Millisecond * 20
  235. // Patch the JWS signature after a short delay
  236. if test.delayedJWSSignaturePatch {
  237. test.configMap.data[bootstrapapi.JWSSignatureKeyPrefix+test.tokenID] = sig
  238. go func() {
  239. time.Sleep(time.Millisecond * 60)
  240. if err := test.configMap.createOrUpdate(client); err != nil {
  241. t.Errorf("could not update the cluster-info ConfigMap with a JWS signature: %v", err)
  242. }
  243. }()
  244. }
  245. // Retrieve validated configuration
  246. kubeconfig, err = retrieveValidatedConfigInfo(client, test.cfg, interval)
  247. if (err != nil) != test.expectedError {
  248. t.Errorf("expected error %v, got %v, error: %v", test.expectedError, err != nil, err)
  249. }
  250. // Return if an error is expected
  251. if test.expectedError {
  252. return
  253. }
  254. // Validate the resulted kubeconfig
  255. kubeconfigBytes, err = clientcmd.Write(*kubeconfig)
  256. if err != nil {
  257. t.Fatalf("cannot marshal resulted kubeconfig %v", err)
  258. }
  259. if string(kubeconfigBytes) != expectedKubeconfig {
  260. t.Error("unexpected kubeconfig")
  261. diff := difflib.UnifiedDiff{
  262. A: difflib.SplitLines(expectedKubeconfig),
  263. B: difflib.SplitLines(string(kubeconfigBytes)),
  264. FromFile: "expected",
  265. ToFile: "got",
  266. Context: 10,
  267. }
  268. diffstr, err := difflib.GetUnifiedDiffString(diff)
  269. if err != nil {
  270. t.Fatalf("error generating unified diff string: %v", err)
  271. }
  272. t.Errorf("\n%s", diffstr)
  273. }
  274. })
  275. }
  276. }
  277. type fakeConfigMap struct {
  278. name string
  279. data map[string]string
  280. }
  281. func (c *fakeConfigMap) createOrUpdate(client clientset.Interface) error {
  282. return apiclient.CreateOrUpdateConfigMap(client, &v1.ConfigMap{
  283. ObjectMeta: metav1.ObjectMeta{
  284. Name: c.name,
  285. Namespace: metav1.NamespacePublic,
  286. },
  287. Data: c.data,
  288. })
  289. }