mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 06:27:05 +00:00
kubeadm: add pubkeypin package (public key pinning hash implementation).
This change adds a `k8s.io/kubernetes/cmd/kubeadm/app/util/pubkeypin` package which implements x509 public key pinning in the style of RFC7469. This is the public key hash format used by the new `kubeadm join --discovery-token-ca-cert-hash` flag. Hashes are namespaced with a short type, with "sha256" being the only currently-supported format. Type "sha256" is a hex-encoded SHA-256 hash over the Subject Public Key Info (SPKI) object in DER-encoded ASN.1.
This commit is contained in:
parent
b86dd9af02
commit
2dd359ba19
@ -55,6 +55,7 @@ filegroup(
|
||||
"//cmd/kubeadm/app/util/apiclient:all-srcs",
|
||||
"//cmd/kubeadm/app/util/config:all-srcs",
|
||||
"//cmd/kubeadm/app/util/kubeconfig:all-srcs",
|
||||
"//cmd/kubeadm/app/util/pubkeypin:all-srcs",
|
||||
"//cmd/kubeadm/app/util/token:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
|
35
cmd/kubeadm/app/util/pubkeypin/BUILD
Normal file
35
cmd/kubeadm/app/util/pubkeypin/BUILD
Normal file
@ -0,0 +1,35 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["pubkeypin_test.go"],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["pubkeypin.go"],
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
108
cmd/kubeadm/app/util/pubkeypin/pubkeypin.go
Normal file
108
cmd/kubeadm/app/util/pubkeypin/pubkeypin.go
Normal file
@ -0,0 +1,108 @@
|
||||
/*
|
||||
Copyright 2017 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 pubkeypin provides primitives for x509 public key pinning in the
|
||||
// style of RFC7469.
|
||||
package pubkeypin
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// formatSHA256 is the prefix for pins that are full-length SHA-256 hashes encoded in base 16 (hex)
|
||||
formatSHA256 = "sha256"
|
||||
)
|
||||
|
||||
// Set is a set of pinned x509 public keys.
|
||||
type Set struct {
|
||||
sha256Hashes map[string]bool
|
||||
}
|
||||
|
||||
// NewSet returns a new, empty PubKeyPinSet
|
||||
func NewSet() *Set {
|
||||
return &Set{make(map[string]bool)}
|
||||
}
|
||||
|
||||
// Allow adds an allowed public key hash to the Set
|
||||
func (s *Set) Allow(pubKeyHashes ...string) error {
|
||||
for _, pubKeyHash := range pubKeyHashes {
|
||||
parts := strings.Split(pubKeyHash, ":")
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid public key hash, expected \"format:value\"")
|
||||
}
|
||||
format, value := parts[0], parts[1]
|
||||
|
||||
switch strings.ToLower(format) {
|
||||
case "sha256":
|
||||
return s.allowSHA256(value)
|
||||
default:
|
||||
return fmt.Errorf("unknown hash format %q", format)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if a certificate matches one of the public keys in the set
|
||||
func (s *Set) Check(certificate *x509.Certificate) error {
|
||||
if s.checkSHA256(certificate) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("public key %s not pinned", Hash(certificate))
|
||||
}
|
||||
|
||||
// Empty returns true if the Set contains no pinned public keys.
|
||||
func (s *Set) Empty() bool {
|
||||
return len(s.sha256Hashes) == 0
|
||||
}
|
||||
|
||||
// Hash calculates the SHA-256 hash of the Subject Public Key Information (SPKI)
|
||||
// object in an x509 certificate (in DER encoding). It returns the full hash as a
|
||||
// hex encoded string (suitable for passing to Set.Allow).
|
||||
func Hash(certificate *x509.Certificate) string {
|
||||
spkiHash := sha256.Sum256(certificate.RawSubjectPublicKeyInfo)
|
||||
return formatSHA256 + ":" + strings.ToLower(hex.EncodeToString(spkiHash[:]))
|
||||
}
|
||||
|
||||
// allowSHA256 validates a "sha256" format hash and adds a canonical version of it into the Set
|
||||
func (s *Set) allowSHA256(hash string) error {
|
||||
// validate that the hash is the right length to be a full SHA-256 hash
|
||||
hashLength := hex.DecodedLen(len(hash))
|
||||
if hashLength != sha256.Size {
|
||||
return fmt.Errorf("expected a %d byte SHA-256 hash, found %d bytes", sha256.Size, hashLength)
|
||||
}
|
||||
|
||||
// validate that the hash is valid hex
|
||||
_, err := hex.DecodeString(hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// in the end, just store the original hex string in memory (in lowercase)
|
||||
s.sha256Hashes[strings.ToLower(hash)] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkSHA256 returns true if the certificate's "sha256" hash is pinned in the Set
|
||||
func (s *Set) checkSHA256(certificate *x509.Certificate) bool {
|
||||
actualHash := sha256.Sum256(certificate.RawSubjectPublicKeyInfo)
|
||||
actualHashHex := strings.ToLower(hex.EncodeToString(actualHash[:]))
|
||||
return s.sha256Hashes[actualHashHex]
|
||||
}
|
158
cmd/kubeadm/app/util/pubkeypin/pubkeypin_test.go
Normal file
158
cmd/kubeadm/app/util/pubkeypin/pubkeypin_test.go
Normal file
@ -0,0 +1,158 @@
|
||||
/*
|
||||
Copyright 2017 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 pubkeypin
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// testCertPEM is a simple self-signed test certificate issued with the openssl CLI:
|
||||
// openssl req -new -newkey rsa:2048 -days 36500 -nodes -x509 -keyout /dev/null -out test.crt
|
||||
const testCertPEM = `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDRDCCAiygAwIBAgIJAJgVaCXvC6HkMA0GCSqGSIb3DQEBBQUAMB8xHTAbBgNV
|
||||
BAMTFGt1YmVhZG0ta2V5cGlucy10ZXN0MCAXDTE3MDcwNTE3NDMxMFoYDzIxMTcw
|
||||
NjExMTc0MzEwWjAfMR0wGwYDVQQDExRrdWJlYWRtLWtleXBpbnMtdGVzdDCCASIw
|
||||
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK0ba8mHU9UtYlzM1Own2Fk/XGjR
|
||||
J4uJQvSeGLtz1hID1IA0dLwruvgLCPadXEOw/f/IWIWcmT+ZmvIHZKa/woq2iHi5
|
||||
+HLhXs7aG4tjKGLYhag1hLjBI7icqV7ovkjdGAt9pWkxEzhIYClFMXDjKpMSynu+
|
||||
YX6nZ9tic1cOkHmx2yiZdMkuriRQnpTOa7bb03OC1VfGl7gHlOAIYaj4539WCOr8
|
||||
+ACTUMJUFEHcRZ2o8a/v6F9GMK+7SC8SJUI+GuroXqlMAdhEv4lX5Co52enYaClN
|
||||
+D9FJLRpBv2YfiCQdJRaiTvCBSxEFz6BN+PtP5l2Hs703ZWEkOqCByM6HV8CAwEA
|
||||
AaOBgDB+MB0GA1UdDgQWBBRQgUX8MhK2rWBWQiPHWcKzoWDH5DBPBgNVHSMESDBG
|
||||
gBRQgUX8MhK2rWBWQiPHWcKzoWDH5KEjpCEwHzEdMBsGA1UEAxMUa3ViZWFkbS1r
|
||||
ZXlwaW5zLXRlc3SCCQCYFWgl7wuh5DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEB
|
||||
BQUAA4IBAQCaAUif7Pfx3X0F08cxhx8/Hdx4jcJw6MCq6iq6rsXM32ge43t8OHKC
|
||||
pJW08dk58a3O1YQSMMvD6GJDAiAfXzfwcwY6j258b1ZlI9Ag0VokvhMl/XfdCsdh
|
||||
AWImnL1t4hvU5jLaImUUMlYxMcSfHBGAm7WJIZ2LdEfg6YWfZh+WGbg1W7uxLxk6
|
||||
y4h5rWdNnzBHWAGf7zJ0oEDV6W6RSwNXtC0JNnLaeIUm/6xdSddJlQPwUv8YH4jX
|
||||
c1vuFqTnJBPcb7W//R/GI2Paicm1cmns9NLnPR35exHxFTy+D1yxmGokpoPMdife
|
||||
aH+sfuxT8xeTPb3kjzF9eJTlnEquUDLM
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
// expectedHash can be verified using the openssl CLI:
|
||||
// openssl x509 -pubkey -in test.crt openssl rsa -pubin -outform der 2>&/dev/null | openssl dgst -sha256 -hex
|
||||
const expectedHash = `sha256:345959acb2c3b2feb87d281961c893f62a314207ef02599f1cc4a5fb255480b3`
|
||||
|
||||
// testCert2PEM is a second test cert generated the same way as testCertPEM
|
||||
const testCert2PEM = `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID9jCCAt6gAwIBAgIJAN5MXZDic7qYMA0GCSqGSIb3DQEBBQUAMFkxCzAJBgNV
|
||||
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCXRlc3RDZXJ0MjAgFw0xNzA3MjQxNjA0
|
||||
MDFaGA8yMTE3MDYzMDE2MDQwMVowWTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNv
|
||||
bWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAG
|
||||
A1UEAxMJdGVzdENlcnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
|
||||
0brwpJYN2ytPWzRBtZSVc3dhkQlA59AzxzqeLLkano0Pxo9NIc3T/y58nnRI8uaS
|
||||
I1P7BzUfJTiUEvmAtX8NggqKK4ld/gPrU+IRww1CUYS4KCkA/0d0ctPy0JwBCjD+
|
||||
b57G3rmNE8c+0jns6J96ZzNtqmv6N+ZlFBAXm1p4S+k0kGi5+hoQ6H7SYXjk2lG+
|
||||
r/8jPQEjy/NSdw1dcCA0Nc6o+hPr32927dS6J9KOhBeXNYUNdbuDDmroM9/gN2e/
|
||||
YMSA1olLeDPQ7Xvhk0PIyEDnHh83AffPCx5yM3htVRGddjIsPAVUJEL3z5leJtxe
|
||||
fzyPghOhHJY0PXqznDQTcwIDAQABo4G+MIG7MB0GA1UdDgQWBBRP0IJqv/5rQ4Uf
|
||||
SByl77dJeEapRDCBiwYDVR0jBIGDMIGAgBRP0IJqv/5rQ4UfSByl77dJeEapRKFd
|
||||
pFswWTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoT
|
||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAxMJdGVzdENlcnQyggkA
|
||||
3kxdkOJzupgwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA0RIMHc10
|
||||
wHHPMh9UflqBgDMF7gfbOL0juJfGloAOcohWWfMZBBJ0CQKMy3xRyoK3HmbW1eeb
|
||||
iATjesw7t4VEAwf7mgKAd+eTfWYB952uq5qYJ2TI28mSofEq1Wz3RmrNkC1KCBs1
|
||||
u+YMFGwyl6necV9zKCeiju4jeovI1GA38TvH7MgYln6vMJ+FbgOXj7XCpek7dQiY
|
||||
KGaeSSH218mGNQaWRQw2Sm3W6cFdANoCJUph4w18s7gjtFpfV63s80hXRps+vEyv
|
||||
jEQMEQpG8Ss7HGJLGLBw/xAmG0e//XS/o2dDonbGbvzToFByz8OGxjMhk6yV6hdd
|
||||
+iyvsLAw/MYMSA==
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
|
||||
// testCert is a small helper to get a test x509.Certificate from the PEM constants
|
||||
func testCert(t *testing.T, pemString string) *x509.Certificate {
|
||||
// Decode the example certificate from a PEM file into a PEM block
|
||||
pemBlock, _ := pem.Decode([]byte(pemString))
|
||||
if pemBlock == nil {
|
||||
t.Fatal("failed to parse test certificate PEM")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse the PEM block into an x509.Certificate
|
||||
result, err := x509.ParseCertificate(pemBlock.Bytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse test certificate: %v", err)
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
s := NewSet()
|
||||
if !s.Empty() {
|
||||
t.Error("expected a new set to be empty")
|
||||
return
|
||||
}
|
||||
err := s.Allow("xyz")
|
||||
if err == nil || !s.Empty() {
|
||||
t.Error("expected allowing junk to fail")
|
||||
return
|
||||
}
|
||||
|
||||
err = s.Allow("0011223344")
|
||||
if err == nil || !s.Empty() {
|
||||
t.Error("expected allowing something too short to fail")
|
||||
return
|
||||
}
|
||||
|
||||
err = s.Allow(expectedHash + expectedHash)
|
||||
if err == nil || !s.Empty() {
|
||||
t.Error("expected allowing something too long to fail")
|
||||
return
|
||||
}
|
||||
|
||||
err = s.Check(testCert(t, testCertPEM))
|
||||
if err == nil {
|
||||
t.Error("expected test cert to not be allowed (yet)")
|
||||
return
|
||||
}
|
||||
|
||||
err = s.Allow(strings.ToUpper(expectedHash))
|
||||
if err != nil || s.Empty() {
|
||||
t.Error("expected allowing uppercase expectedHash to succeed")
|
||||
return
|
||||
}
|
||||
|
||||
err = s.Check(testCert(t, testCertPEM))
|
||||
if err != nil {
|
||||
t.Errorf("expected test cert to be allowed, but got back: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = s.Check(testCert(t, testCert2PEM))
|
||||
if err == nil {
|
||||
t.Error("expected the second test cert to be disallowed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestHash(t *testing.T) {
|
||||
actualHash := Hash(testCert(t, testCertPEM))
|
||||
if actualHash != expectedHash {
|
||||
t.Errorf(
|
||||
"failed to Hash() to the expected value\n\texpected: %q\n\t actual: %q",
|
||||
expectedHash,
|
||||
actualHash,
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user