smallstep-certificates/authority/provisioner/utils_test.go
David Cowden 51f16ee2e0 aws: add tests covering metadata service versions
* Add constructor tests for the aws provisioner.
* Add a test to make sure the "v1" logic continues to work.

By and large, v2 is the way to go. However, there are some instances of
things that specifically request metadata service version 1 and so this
adds minimal coverage to make sure we don't accidentally break the path
should anyone need to depend on the former logic.
2020-07-22 16:52:06 -07:00

1112 lines
31 KiB
Go

package provisioner
import (
"crypto"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"time"
"github.com/pkg/errors"
"github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/crypto/randutil"
"github.com/smallstep/cli/jose"
"golang.org/x/crypto/ssh"
)
var (
defaultDisableRenewal = false
defaultEnableSSHCA = true
globalProvisionerClaims = Claims{
MinTLSDur: &Duration{5 * time.Minute},
MaxTLSDur: &Duration{24 * time.Hour},
DefaultTLSDur: &Duration{24 * time.Hour},
DisableRenewal: &defaultDisableRenewal,
MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs
MaxUserSSHDur: &Duration{Duration: 24 * time.Hour},
DefaultUserSSHDur: &Duration{Duration: 16 * time.Hour},
MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs
MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour},
DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour},
EnableSSHCA: &defaultEnableSSHCA,
}
testAudiences = Audiences{
Sign: []string{"https://ca.smallstep.com/1.0/sign", "https://ca.smallstep.com/sign"},
Revoke: []string{"https://ca.smallstep.com/1.0/revoke", "https://ca.smallstep.com/revoke"},
SSHSign: []string{"https://ca.smallstep.com/1.0/ssh/sign"},
SSHRevoke: []string{"https://ca.smallstep.com/1.0/ssh/revoke"},
SSHRenew: []string{"https://ca.smallstep.com/1.0/ssh/renew"},
SSHRekey: []string{"https://ca.smallstep.com/1.0/ssh/rekey"},
}
)
const awsTestCertificate = `-----BEGIN CERTIFICATE-----
MIICFTCCAX6gAwIBAgIRAKmbVVYAl/1XEqRfF3eJ97MwDQYJKoZIhvcNAQELBQAw
GDEWMBQGA1UEAxMNQVdTIFRlc3QgQ2VydDAeFw0xOTA0MjQyMjU3MzlaFw0yOTA0
MjEyMjU3MzlaMBgxFjAUBgNVBAMTDUFXUyBUZXN0IENlcnQwgZ8wDQYJKoZIhvcN
AQEBBQADgY0AMIGJAoGBAOHMmMXwbXN90SoRl/xXAcJs5TacaVYJ5iNAVWM5KYyF
+JwqYuJp/umLztFUi0oX0luu3EzD4KurVeUJSzZjTFTX1d/NX6hA45+bvdSUOcgV
UghO+2uhBZ4SNFxFRZ7SKvoWIN195l5bVX6/60Eo6+kUCKCkyxW4V/ksWzdXjHnf
AgMBAAGjXzBdMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMB0G
A1UdDgQWBBRHfLOjEddK/CWCIHNg8Oc/oJa1IzAYBgNVHREEETAPgg1BV1MgVGVz
dCBDZXJ0MA0GCSqGSIb3DQEBCwUAA4GBAKNCiVM9eGb9dW2xNyHaHAmmy7ERB2OJ
7oXHfLjooOavk9lU/Gs2jfX/JSBa84+DzWg9ShmCNLti8CxU/dhzXW7jE/5CcdTa
DCA6B3Yl5TmfG9+D9dtFqRB2CiMgNcsJJE5Dc6pDwBIiSj/MkE0AaGVQmSwn6Cb6
vX1TAxqeWJHq
-----END CERTIFICATE-----`
const awsTestKey = `-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDhzJjF8G1zfdEqEZf8VwHCbOU2nGlWCeYjQFVjOSmMhficKmLi
af7pi87RVItKF9JbrtxMw+Crq1XlCUs2Y0xU19XfzV+oQOOfm73UlDnIFVIITvtr
oQWeEjRcRUWe0ir6FiDdfeZeW1V+v+tBKOvpFAigpMsVuFf5LFs3V4x53wIDAQAB
AoGADZQFF9oWatyFCHeYYSdGRs/PlNIhD3h262XB/L6CPh4MTi/KVH01RAwROstP
uPvnvXWtb7xTtV8PQj+l0zZzb4W/DLCSBdoRwpuNXyffUCtbI22jPupTsVu+ENWR
3x7HHzoZYjU45ADSTMxEtwD7/zyNgpRKjIA2HYpkt+fI27ECQQD5/AOr9/yQD73x
cquF+FWahWgDL25YeMwdfe1HfpUxUxd9kJJKieB8E2BtBAv9XNguxIBpf7VlAKsF
NFhdfWFHAkEA5zuX8vqDecSzyNNEQd3tugxt1pGOXNesHzuPbdlw3ppN9Rbd93an
uU2TaAvTjr/3EkxulYNRmHs+RSVK54+uqQJAKWurhBQMAibJlzcj2ofiTz8pk9WJ
GBmz4HMcHMuJlumoq8KHqtgbnRNs18Ni5TE8FMu0Z0ak3L52l98rgRokQwJBAJS8
9KTLF79AFBVeME3eH4jJbe3TeyulX4ZHnZ8fe0b1IqhAqU8A+CpuCB+pW9A7Ewam
O4vZCKd4vzljH6eL+OECQHHxhYoTW7lFpKGnUDG9fPZ3eYzWpgka6w1vvBk10BAu
6fbwppM9pQ7DPMg7V6YGEjjT0gX9B9TttfHxGhvtZNQ=
-----END RSA PRIVATE KEY-----`
func must(args ...interface{}) []interface{} {
if l := len(args); l > 0 && args[l-1] != nil {
if err, ok := args[l-1].(error); ok {
panic(err)
}
}
return args
}
func generateJSONWebKey() (*jose.JSONWebKey, error) {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
if err != nil {
return nil, err
}
fp, err := jwk.Thumbprint(crypto.SHA256)
if err != nil {
return nil, err
}
jwk.KeyID = string(hex.EncodeToString(fp))
return jwk, nil
}
func generateJSONWebKeySet(n int) (jose.JSONWebKeySet, error) {
var keySet jose.JSONWebKeySet
for i := 0; i < n; i++ {
key, err := generateJSONWebKey()
if err != nil {
return jose.JSONWebKeySet{}, err
}
keySet.Keys = append(keySet.Keys, *key)
}
return keySet, nil
}
func encryptJSONWebKey(jwk *jose.JSONWebKey) (*jose.JSONWebEncryption, error) {
b, err := json.Marshal(jwk)
if err != nil {
return nil, err
}
salt, err := randutil.Salt(jose.PBKDF2SaltSize)
if err != nil {
return nil, err
}
opts := new(jose.EncrypterOptions)
opts.WithContentType(jose.ContentType("jwk+json"))
recipient := jose.Recipient{
Algorithm: jose.PBES2_HS256_A128KW,
Key: []byte("password"),
PBES2Count: jose.PBKDF2Iterations,
PBES2Salt: salt,
}
encrypter, err := jose.NewEncrypter(jose.DefaultEncAlgorithm, recipient, opts)
if err != nil {
return nil, err
}
return encrypter.Encrypt(b)
}
func decryptJSONWebKey(key string) (*jose.JSONWebKey, error) {
enc, err := jose.ParseEncrypted(key)
if err != nil {
return nil, err
}
b, err := enc.Decrypt([]byte("password"))
if err != nil {
return nil, err
}
jwk := new(jose.JSONWebKey)
if err := json.Unmarshal(b, jwk); err != nil {
return nil, err
}
return jwk, nil
}
func generateJWK() (*JWK, error) {
name, err := randutil.Alphanumeric(10)
if err != nil {
return nil, err
}
jwk, err := generateJSONWebKey()
if err != nil {
return nil, err
}
jwe, err := encryptJSONWebKey(jwk)
if err != nil {
return nil, err
}
public := jwk.Public()
encrypted, err := jwe.CompactSerialize()
if err != nil {
return nil, err
}
claimer, err := NewClaimer(nil, globalProvisionerClaims)
if err != nil {
return nil, err
}
return &JWK{
Name: name,
Type: "JWK",
Key: &public,
EncryptedKey: encrypted,
Claims: &globalProvisionerClaims,
audiences: testAudiences,
claimer: claimer,
}, nil
}
func generateK8sSA(inputPubKey interface{}) (*K8sSA, error) {
fooPubB, err := ioutil.ReadFile("./testdata/certs/foo.pub")
if err != nil {
return nil, err
}
fooPub, err := pemutil.ParseKey(fooPubB)
if err != nil {
return nil, err
}
barPubB, err := ioutil.ReadFile("./testdata/certs/bar.pub")
if err != nil {
return nil, err
}
barPub, err := pemutil.ParseKey(barPubB)
if err != nil {
return nil, err
}
claimer, err := NewClaimer(nil, globalProvisionerClaims)
if err != nil {
return nil, err
}
pubKeys := []interface{}{fooPub, barPub}
if inputPubKey != nil {
pubKeys = append(pubKeys, inputPubKey)
}
return &K8sSA{
Name: K8sSAName,
Type: "K8sSA",
Claims: &globalProvisionerClaims,
audiences: testAudiences,
claimer: claimer,
pubKeys: pubKeys,
}, nil
}
func generateSSHPOP() (*SSHPOP, error) {
name, err := randutil.Alphanumeric(10)
if err != nil {
return nil, err
}
claimer, err := NewClaimer(nil, globalProvisionerClaims)
if err != nil {
return nil, err
}
userB, err := ioutil.ReadFile("./testdata/certs/ssh_user_ca_key.pub")
if err != nil {
return nil, err
}
userKey, _, _, _, err := ssh.ParseAuthorizedKey(userB)
if err != nil {
return nil, err
}
hostB, err := ioutil.ReadFile("./testdata/certs/ssh_host_ca_key.pub")
if err != nil {
return nil, err
}
hostKey, _, _, _, err := ssh.ParseAuthorizedKey(hostB)
if err != nil {
return nil, err
}
return &SSHPOP{
Name: name,
Type: "SSHPOP",
Claims: &globalProvisionerClaims,
audiences: testAudiences,
claimer: claimer,
sshPubKeys: &SSHKeys{
UserKeys: []ssh.PublicKey{userKey},
HostKeys: []ssh.PublicKey{hostKey},
},
}, nil
}
func generateX5C(root []byte) (*X5C, error) {
if root == nil {
root = []byte(`-----BEGIN CERTIFICATE-----
MIIBhTCCASqgAwIBAgIRAMalM7pKi0GCdKjO6u88OyowCgYIKoZIzj0EAwIwFDES
MBAGA1UEAxMJcm9vdC10ZXN0MCAXDTE5MTAwMjAyMzk0OFoYDzIxMTkwOTA4MDIz
OTQ4WjAUMRIwEAYDVQQDEwlyb290LXRlc3QwWTATBgcqhkjOPQIBBggqhkjOPQMB
BwNCAAS29QTCXUu7cx9sa9wZPpRSFq/zXaw8Ai3EIygayrBsKnX42U2atBUjcBZO
BWL6A+PpLzU9ja867U5SYNHERS+Oo1swWTAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0T
AQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowFAYD
VR0RBA0wC4IJcm9vdC10ZXN0MAoGCCqGSM49BAMCA0kAMEYCIQC2vgqwla0u8LHH
1MHob14qvS5o76HautbIBW7fcHzz5gIhAIx5A2+wkJYX4026kqaZCk/1sAwTxSGY
M46l92gdOozT
-----END CERTIFICATE-----`)
}
name, err := randutil.Alphanumeric(10)
if err != nil {
return nil, err
}
claimer, err := NewClaimer(nil, globalProvisionerClaims)
if err != nil {
return nil, err
}
rootPool := x509.NewCertPool()
var (
block *pem.Block
rest = root
)
for rest != nil {
block, rest = pem.Decode(rest)
if block == nil {
break
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, errors.Wrap(err, "error parsing x509 certificate from PEM block")
}
rootPool.AddCert(cert)
}
return &X5C{
Name: name,
Type: "X5C",
Roots: root,
Claims: &globalProvisionerClaims,
audiences: testAudiences,
claimer: claimer,
rootPool: rootPool,
}, nil
}
func generateOIDC() (*OIDC, error) {
name, err := randutil.Alphanumeric(10)
if err != nil {
return nil, err
}
clientID, err := randutil.Alphanumeric(10)
if err != nil {
return nil, err
}
issuer, err := randutil.Alphanumeric(10)
if err != nil {
return nil, err
}
jwk, err := generateJSONWebKey()
if err != nil {
return nil, err
}
claimer, err := NewClaimer(nil, globalProvisionerClaims)
if err != nil {
return nil, err
}
return &OIDC{
Name: name,
Type: "OIDC",
ClientID: clientID,
ConfigurationEndpoint: "https://example.com/.well-known/openid-configuration",
Claims: &globalProvisionerClaims,
configuration: openIDConfiguration{
Issuer: issuer,
JWKSetURI: "https://example.com/.well-known/jwks",
},
keyStore: &keyStore{
keySet: jose.JSONWebKeySet{Keys: []jose.JSONWebKey{*jwk}},
expiry: time.Now().Add(24 * time.Hour),
},
claimer: claimer,
}, nil
}
func generateGCP() (*GCP, error) {
name, err := randutil.Alphanumeric(10)
if err != nil {
return nil, err
}
serviceAccount, err := randutil.Alphanumeric(10)
if err != nil {
return nil, err
}
jwk, err := generateJSONWebKey()
if err != nil {
return nil, err
}
claimer, err := NewClaimer(nil, globalProvisionerClaims)
if err != nil {
return nil, err
}
return &GCP{
Type: "GCP",
Name: name,
ServiceAccounts: []string{serviceAccount},
Claims: &globalProvisionerClaims,
claimer: claimer,
config: newGCPConfig(),
keyStore: &keyStore{
keySet: jose.JSONWebKeySet{Keys: []jose.JSONWebKey{*jwk}},
expiry: time.Now().Add(24 * time.Hour),
},
audiences: testAudiences.WithFragment("gcp/" + name),
}, nil
}
func generateAWS() (*AWS, error) {
name, err := randutil.Alphanumeric(10)
if err != nil {
return nil, err
}
accountID, err := randutil.Alphanumeric(10)
if err != nil {
return nil, err
}
claimer, err := NewClaimer(nil, globalProvisionerClaims)
if err != nil {
return nil, err
}
block, _ := pem.Decode([]byte(awsTestCertificate))
if block == nil || block.Type != "CERTIFICATE" {
return nil, errors.New("error decoding AWS certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, errors.Wrap(err, "error parsing AWS certificate")
}
return &AWS{
Type: "AWS",
Name: name,
Accounts: []string{accountID},
Claims: &globalProvisionerClaims,
IMDSVersions: []string{"v2", "v1"},
claimer: claimer,
config: &awsConfig{
identityURL: awsIdentityURL,
signatureURL: awsSignatureURL,
tokenURL: awsAPITokenURL,
tokenTTL: awsAPITokenTTL,
certificate: cert,
signatureAlgorithm: awsSignatureAlgorithm,
},
audiences: testAudiences.WithFragment("aws/" + name),
}, nil
}
func generateAWSWithServer() (*AWS, *httptest.Server, error) {
aws, err := generateAWS()
if err != nil {
return nil, nil, err
}
block, _ := pem.Decode([]byte(awsTestKey))
if block == nil || block.Type != "RSA PRIVATE KEY" {
return nil, nil, errors.New("error decoding AWS key")
}
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, nil, errors.Wrap(err, "error parsing AWS private key")
}
doc, err := json.MarshalIndent(awsInstanceIdentityDocument{
AccountID: aws.Accounts[0],
Architecture: "x86_64",
AvailabilityZone: "us-west-2b",
ImageID: "image-id",
InstanceID: "instance-id",
InstanceType: "t2.micro",
PendingTime: time.Now(),
PrivateIP: "127.0.0.1",
Region: "us-west-1",
Version: "2017-09-30",
}, "", " ")
if err != nil {
return nil, nil, err
}
sum := sha256.Sum256(doc)
signature, err := key.Sign(rand.Reader, sum[:], crypto.SHA256)
if err != nil {
return nil, nil, errors.Wrap(err, "error signing document")
}
token := "AQAEAEEO9-7Z88ewKFpboZuDlFYWz9A3AN-wMOVzjEhfAyXW31BvVw=="
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/latest/dynamic/instance-identity/document":
// check for API token
if r.Header.Get("X-aws-ec2-metadata-token") != token {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("401 Unauthorized"))
}
w.Write(doc)
case "/latest/dynamic/instance-identity/signature":
// check for API token
if r.Header.Get("X-aws-ec2-metadata-token") != token {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("401 Unauthorized"))
}
w.Write([]byte(base64.StdEncoding.EncodeToString(signature)))
case "/latest/api/token":
w.Write([]byte(token))
case "/bad-document":
w.Write([]byte("{}"))
case "/bad-signature":
w.Write([]byte("YmFkLXNpZ25hdHVyZQo="))
case "/bad-json":
w.Write([]byte("{"))
default:
http.NotFound(w, r)
}
}))
aws.config.identityURL = srv.URL + "/latest/dynamic/instance-identity/document"
aws.config.signatureURL = srv.URL + "/latest/dynamic/instance-identity/signature"
aws.config.tokenURL = srv.URL + "/latest/api/token"
return aws, srv, nil
}
func generateAWSV1Only() (*AWS, error) {
name, err := randutil.Alphanumeric(10)
if err != nil {
return nil, err
}
accountID, err := randutil.Alphanumeric(10)
if err != nil {
return nil, err
}
claimer, err := NewClaimer(nil, globalProvisionerClaims)
if err != nil {
return nil, err
}
block, _ := pem.Decode([]byte(awsTestCertificate))
if block == nil || block.Type != "CERTIFICATE" {
return nil, errors.New("error decoding AWS certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, errors.Wrap(err, "error parsing AWS certificate")
}
return &AWS{
Type: "AWS",
Name: name,
Accounts: []string{accountID},
Claims: &globalProvisionerClaims,
IMDSVersions: []string{"v1"},
claimer: claimer,
config: &awsConfig{
identityURL: awsIdentityURL,
signatureURL: awsSignatureURL,
tokenURL: awsAPITokenURL,
tokenTTL: awsAPITokenTTL,
certificate: cert,
signatureAlgorithm: awsSignatureAlgorithm,
},
audiences: testAudiences.WithFragment("aws/" + name),
}, nil
}
func generateAWSWithServerV1Only() (*AWS, *httptest.Server, error) {
aws, err := generateAWSV1Only()
if err != nil {
return nil, nil, err
}
block, _ := pem.Decode([]byte(awsTestKey))
if block == nil || block.Type != "RSA PRIVATE KEY" {
return nil, nil, errors.New("error decoding AWS key")
}
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, nil, errors.Wrap(err, "error parsing AWS private key")
}
doc, err := json.MarshalIndent(awsInstanceIdentityDocument{
AccountID: aws.Accounts[0],
Architecture: "x86_64",
AvailabilityZone: "us-west-2b",
ImageID: "image-id",
InstanceID: "instance-id",
InstanceType: "t2.micro",
PendingTime: time.Now(),
PrivateIP: "127.0.0.1",
Region: "us-west-1",
Version: "2017-09-30",
}, "", " ")
if err != nil {
return nil, nil, err
}
sum := sha256.Sum256(doc)
signature, err := key.Sign(rand.Reader, sum[:], crypto.SHA256)
if err != nil {
return nil, nil, errors.Wrap(err, "error signing document")
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/latest/dynamic/instance-identity/document":
w.Write(doc)
case "/latest/dynamic/instance-identity/signature":
w.Write([]byte(base64.StdEncoding.EncodeToString(signature)))
case "/bad-document":
w.Write([]byte("{}"))
case "/bad-signature":
w.Write([]byte("YmFkLXNpZ25hdHVyZQo="))
case "/bad-json":
w.Write([]byte("{"))
default:
http.NotFound(w, r)
}
}))
aws.config.identityURL = srv.URL + "/latest/dynamic/instance-identity/document"
aws.config.signatureURL = srv.URL + "/latest/dynamic/instance-identity/signature"
return aws, srv, nil
}
func generateAzure() (*Azure, error) {
name, err := randutil.Alphanumeric(10)
if err != nil {
return nil, err
}
tenantID, err := randutil.Alphanumeric(10)
if err != nil {
return nil, err
}
claimer, err := NewClaimer(nil, globalProvisionerClaims)
if err != nil {
return nil, err
}
jwk, err := generateJSONWebKey()
if err != nil {
return nil, err
}
return &Azure{
Type: "Azure",
Name: name,
TenantID: tenantID,
Audience: azureDefaultAudience,
Claims: &globalProvisionerClaims,
claimer: claimer,
config: newAzureConfig(tenantID),
oidcConfig: openIDConfiguration{
Issuer: "https://sts.windows.net/" + tenantID + "/",
JWKSetURI: "https://login.microsoftonline.com/common/discovery/keys",
},
keyStore: &keyStore{
keySet: jose.JSONWebKeySet{Keys: []jose.JSONWebKey{*jwk}},
expiry: time.Now().Add(24 * time.Hour),
},
}, nil
}
func generateAzureWithServer() (*Azure, *httptest.Server, error) {
az, err := generateAzure()
if err != nil {
return nil, nil, err
}
writeJSON := func(w http.ResponseWriter, v interface{}) {
b, err := json.Marshal(v)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(b)
}
getPublic := func(ks jose.JSONWebKeySet) jose.JSONWebKeySet {
var ret jose.JSONWebKeySet
for _, k := range ks.Keys {
ret.Keys = append(ret.Keys, k.Public())
}
return ret
}
issuer := "https://sts.windows.net/" + az.TenantID + "/"
srv := httptest.NewUnstartedServer(nil)
srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/error":
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
case "/" + az.TenantID + "/.well-known/openid-configuration":
writeJSON(w, openIDConfiguration{Issuer: issuer, JWKSetURI: srv.URL + "/jwks_uri"})
case "/openid-configuration-no-issuer":
writeJSON(w, openIDConfiguration{Issuer: "", JWKSetURI: srv.URL + "/jwks_uri"})
case "/openid-configuration-fail-jwk":
writeJSON(w, openIDConfiguration{Issuer: issuer, JWKSetURI: srv.URL + "/error"})
case "/random":
keySet := must(generateJSONWebKeySet(2))[0].(jose.JSONWebKeySet)
w.Header().Add("Cache-Control", "max-age=5")
writeJSON(w, getPublic(keySet))
case "/private":
writeJSON(w, az.keyStore.keySet)
case "/jwks_uri":
w.Header().Add("Cache-Control", "max-age=5")
writeJSON(w, getPublic(az.keyStore.keySet))
case "/metadata/identity/oauth2/token":
tok, err := generateAzureToken("subject", issuer, "https://management.azure.com/", az.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", time.Now(), &az.keyStore.keySet.Keys[0])
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
writeJSON(w, azureIdentityToken{
AccessToken: tok,
})
}
default:
http.NotFound(w, r)
}
})
srv.Start()
az.config.oidcDiscoveryURL = srv.URL + "/" + az.TenantID + "/.well-known/openid-configuration"
az.config.identityTokenURL = srv.URL + "/metadata/identity/oauth2/token"
return az, srv, nil
}
func generateCollection(nJWK, nOIDC int) (*Collection, error) {
col := NewCollection(testAudiences)
for i := 0; i < nJWK; i++ {
p, err := generateJWK()
if err != nil {
return nil, err
}
col.Store(p)
}
for i := 0; i < nOIDC; i++ {
p, err := generateOIDC()
if err != nil {
return nil, err
}
col.Store(p)
}
return col, nil
}
func generateSimpleToken(iss, aud string, jwk *jose.JSONWebKey) (string, error) {
return generateToken("subject", iss, aud, "name@smallstep.com", []string{"test.smallstep.com"}, time.Now(), jwk)
}
type tokOption func(*jose.SignerOptions) error
func withX5CHdr(certs []*x509.Certificate) tokOption {
return func(so *jose.SignerOptions) error {
strs := make([]string, len(certs))
for i, cert := range certs {
strs[i] = base64.StdEncoding.EncodeToString(cert.Raw)
}
so.WithHeader("x5c", strs)
return nil
}
}
func withSSHPOPFile(cert *ssh.Certificate) tokOption {
return func(so *jose.SignerOptions) error {
so.WithHeader("sshpop", base64.StdEncoding.EncodeToString(cert.Marshal()))
return nil
}
}
func generateToken(sub, iss, aud string, email string, sans []string, iat time.Time, jwk *jose.JSONWebKey, tokOpts ...tokOption) (string, error) {
so := new(jose.SignerOptions)
so.WithType("JWT")
so.WithHeader("kid", jwk.KeyID)
for _, o := range tokOpts {
if err := o(so); err != nil {
return "", err
}
}
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so)
if err != nil {
return "", err
}
id, err := randutil.ASCII(64)
if err != nil {
return "", err
}
claims := struct {
jose.Claims
Email string `json:"email"`
SANS []string `json:"sans"`
}{
Claims: jose.Claims{
ID: id,
Subject: sub,
Issuer: iss,
IssuedAt: jose.NewNumericDate(iat),
NotBefore: jose.NewNumericDate(iat),
Expiry: jose.NewNumericDate(iat.Add(5 * time.Minute)),
Audience: []string{aud},
},
Email: email,
SANS: sans,
}
return jose.Signed(sig).Claims(claims).CompactSerialize()
}
func generateX5CSSHToken(jwk *jose.JSONWebKey, claims *x5cPayload, tokOpts ...tokOption) (string, error) {
so := new(jose.SignerOptions)
so.WithType("JWT")
so.WithHeader("kid", jwk.KeyID)
for _, o := range tokOpts {
if err := o(so); err != nil {
return "", err
}
}
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so)
if err != nil {
return "", err
}
return jose.Signed(sig).Claims(claims).CompactSerialize()
}
func getK8sSAPayload() *k8sSAPayload {
return &k8sSAPayload{
Claims: jose.Claims{
Issuer: k8sSAIssuer,
Subject: "foo",
},
Namespace: "ns-foo",
SecretName: "sn-foo",
ServiceAccountName: "san-foo",
ServiceAccountUID: "sauid-foo",
}
}
func generateK8sSAToken(jwk *jose.JSONWebKey, claims *k8sSAPayload, tokOpts ...tokOption) (string, error) {
so := new(jose.SignerOptions)
so.WithHeader("kid", jwk.KeyID)
for _, o := range tokOpts {
if err := o(so); err != nil {
return "", err
}
}
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so)
if err != nil {
return "", err
}
if claims == nil {
claims = getK8sSAPayload()
}
return jose.Signed(sig).Claims(*claims).CompactSerialize()
}
func generateSimpleSSHUserToken(iss, aud string, jwk *jose.JSONWebKey) (string, error) {
return generateSSHToken("subject@localhost", iss, aud, time.Now(), &SSHOptions{
CertType: "user",
Principals: []string{"name"},
}, jwk)
}
func generateSimpleSSHHostToken(iss, aud string, jwk *jose.JSONWebKey) (string, error) {
return generateSSHToken("subject@localhost", iss, aud, time.Now(), &SSHOptions{
CertType: "host",
Principals: []string{"smallstep.com"},
}, jwk)
}
func generateSSHToken(sub, iss, aud string, iat time.Time, sshOpts *SSHOptions, jwk *jose.JSONWebKey) (string, error) {
sig, err := jose.NewSigner(
jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
new(jose.SignerOptions).WithType("JWT").WithHeader("kid", jwk.KeyID),
)
if err != nil {
return "", err
}
id, err := randutil.ASCII(64)
if err != nil {
return "", err
}
claims := struct {
jose.Claims
Step *stepPayload `json:"step,omitempty"`
}{
Claims: jose.Claims{
ID: id,
Subject: sub,
Issuer: iss,
IssuedAt: jose.NewNumericDate(iat),
NotBefore: jose.NewNumericDate(iat),
Expiry: jose.NewNumericDate(iat.Add(5 * time.Minute)),
Audience: []string{aud},
},
Step: &stepPayload{
SSH: sshOpts,
},
}
return jose.Signed(sig).Claims(claims).CompactSerialize()
}
func generateGCPToken(sub, iss, aud, instanceID, instanceName, projectID, zone string, iat time.Time, jwk *jose.JSONWebKey) (string, error) {
sig, err := jose.NewSigner(
jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
new(jose.SignerOptions).WithType("JWT").WithHeader("kid", jwk.KeyID),
)
if err != nil {
return "", err
}
aud, err = generateSignAudience("https://ca.smallstep.com", aud)
if err != nil {
return "", err
}
claims := gcpPayload{
Claims: jose.Claims{
Subject: sub,
Issuer: iss,
IssuedAt: jose.NewNumericDate(iat),
NotBefore: jose.NewNumericDate(iat),
Expiry: jose.NewNumericDate(iat.Add(5 * time.Minute)),
Audience: []string{aud},
},
AuthorizedParty: sub,
Email: "foo@developer.gserviceaccount.com",
EmailVerified: true,
Google: gcpGooglePayload{
ComputeEngine: gcpComputeEnginePayload{
InstanceID: instanceID,
InstanceName: instanceName,
InstanceCreationTimestamp: jose.NewNumericDate(iat),
ProjectID: projectID,
ProjectNumber: 1234567890,
Zone: zone,
},
},
}
return jose.Signed(sig).Claims(claims).CompactSerialize()
}
func generateAWSToken(sub, iss, aud, accountID, instanceID, privateIP, region string, iat time.Time, key crypto.Signer) (string, error) {
doc, err := json.MarshalIndent(awsInstanceIdentityDocument{
AccountID: accountID,
Architecture: "x86_64",
AvailabilityZone: "us-west-2b",
ImageID: "ami-123123",
InstanceID: instanceID,
InstanceType: "t2.micro",
PendingTime: iat,
PrivateIP: privateIP,
Region: region,
Version: "2017-09-30",
}, "", " ")
if err != nil {
return "", err
}
sum := sha256.Sum256(doc)
signature, err := key.Sign(rand.Reader, sum[:], crypto.SHA256)
if err != nil {
return "", errors.Wrap(err, "error signing document")
}
sig, err := jose.NewSigner(
jose.SigningKey{Algorithm: jose.HS256, Key: signature},
new(jose.SignerOptions).WithType("JWT"),
)
if err != nil {
return "", err
}
aud, err = generateSignAudience("https://ca.smallstep.com", aud)
if err != nil {
return "", err
}
claims := awsPayload{
Claims: jose.Claims{
Subject: sub,
Issuer: iss,
IssuedAt: jose.NewNumericDate(iat),
NotBefore: jose.NewNumericDate(iat),
Expiry: jose.NewNumericDate(iat.Add(5 * time.Minute)),
Audience: []string{aud},
},
Amazon: awsAmazonPayload{
Document: doc,
Signature: signature,
},
}
return jose.Signed(sig).Claims(claims).CompactSerialize()
}
func generateAzureToken(sub, iss, aud, tenantID, subscriptionID, resourceGroup, virtualMachine string, iat time.Time, jwk *jose.JSONWebKey) (string, error) {
sig, err := jose.NewSigner(
jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
new(jose.SignerOptions).WithType("JWT").WithHeader("kid", jwk.KeyID),
)
if err != nil {
return "", err
}
claims := azurePayload{
Claims: jose.Claims{
Subject: sub,
Issuer: iss,
IssuedAt: jose.NewNumericDate(iat),
NotBefore: jose.NewNumericDate(iat),
Expiry: jose.NewNumericDate(iat.Add(5 * time.Minute)),
Audience: []string{aud},
ID: "the-jti",
},
AppID: "the-appid",
AppIDAcr: "the-appidacr",
IdentityProvider: "the-idp",
ObjectID: "the-oid",
TenantID: tenantID,
Version: "the-version",
XMSMirID: fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/virtualMachines/%s", subscriptionID, resourceGroup, virtualMachine),
}
return jose.Signed(sig).Claims(claims).CompactSerialize()
}
func parseToken(token string) (*jose.JSONWebToken, *jose.Claims, error) {
tok, err := jose.ParseSigned(token)
if err != nil {
return nil, nil, err
}
claims := new(jose.Claims)
if err := tok.UnsafeClaimsWithoutVerification(claims); err != nil {
return nil, nil, err
}
return tok, claims, nil
}
func parseAWSToken(token string) (*jose.JSONWebToken, *awsPayload, error) {
tok, err := jose.ParseSigned(token)
if err != nil {
return nil, nil, err
}
claims := new(awsPayload)
if err := tok.UnsafeClaimsWithoutVerification(claims); err != nil {
return nil, nil, err
}
var doc awsInstanceIdentityDocument
if err := json.Unmarshal(claims.Amazon.Document, &doc); err != nil {
return nil, nil, errors.Wrap(err, "error unmarshaling identity document")
}
claims.document = doc
return tok, claims, nil
}
func generateJWKServer(n int) *httptest.Server {
hits := struct {
Hits int `json:"hits"`
}{}
writeJSON := func(w http.ResponseWriter, v interface{}) {
b, err := json.Marshal(v)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(b)
}
getPublic := func(ks jose.JSONWebKeySet) jose.JSONWebKeySet {
var ret jose.JSONWebKeySet
for _, k := range ks.Keys {
ret.Keys = append(ret.Keys, k.Public())
}
return ret
}
defaultKeySet := must(generateJSONWebKeySet(n))[0].(jose.JSONWebKeySet)
srv := httptest.NewUnstartedServer(nil)
srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hits.Hits++
switch r.RequestURI {
case "/error":
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
case "/hits":
writeJSON(w, hits)
case "/.well-known/openid-configuration":
writeJSON(w, openIDConfiguration{Issuer: "the-issuer", JWKSetURI: srv.URL + "/jwks_uri"})
case "/common/.well-known/openid-configuration":
writeJSON(w, openIDConfiguration{Issuer: "https://login.microsoftonline.com/{tenantid}/v2.0", JWKSetURI: srv.URL + "/jwks_uri"})
case "/random":
keySet := must(generateJSONWebKeySet(n))[0].(jose.JSONWebKeySet)
w.Header().Add("Cache-Control", "max-age=5")
writeJSON(w, getPublic(keySet))
case "/no-cache":
keySet := must(generateJSONWebKeySet(n))[0].(jose.JSONWebKeySet)
w.Header().Add("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate")
writeJSON(w, getPublic(keySet))
case "/private":
writeJSON(w, defaultKeySet)
default:
w.Header().Add("Cache-Control", "max-age=5")
writeJSON(w, getPublic(defaultKeySet))
}
})
srv.Start()
return srv
}
func generateACME() (*ACME, error) {
// Initialize provisioners
p := &ACME{
Type: "ACME",
Name: "test@acme-provisioner.com",
}
if err := p.Init(Config{Claims: globalProvisionerClaims}); err != nil {
return nil, err
}
return p, nil
}
func parseCerts(b []byte) ([]*x509.Certificate, error) {
var (
block *pem.Block
rest = b
certs = []*x509.Certificate{}
)
for rest != nil {
block, rest = pem.Decode(rest)
if block == nil {
break
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, errors.Wrap(err, "error parsing x509 certificate from PEM block")
}
certs = append(certs, cert)
}
return certs, nil
}