├── charts ├── templates │ ├── NOTES.txt │ └── tcs_issuer.yaml ├── Chart.yaml ├── values.yaml └── .helmignore ├── docs ├── images │ ├── tcs-kubernetes.png │ ├── tcs-kubernetes-csr.png │ └── tcs-kubernetes-certmanager.png ├── azure.md ├── quote-attestation-api.md ├── istio-csr-external-ca-setup.md ├── istio-custom-ca-with-csr.md └── integrate-key-server.md ├── config ├── manager │ ├── .env.secret │ ├── tcs_issuer_config.yaml │ ├── kustomization.yaml │ └── tcs_issuer.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── service_account.yaml │ ├── auth_proxy_client_clusterrole.yaml │ ├── auth_proxy_service.yaml │ ├── role_binding.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_role.yaml │ ├── leader_election_role_binding.yaml │ ├── leader_election_role.yaml │ ├── kustomization.yaml │ └── role.yaml ├── crd │ ├── patches │ │ ├── cainjection_in_quoteattestations.yaml │ │ └── webhook_in_quoteattestations.yaml │ ├── kustomizeconfig.yaml │ ├── kustomization.yaml │ ├── tcs.intel.com_tcsissuers.yaml │ └── tcs.intel.com_tcsclusterissuers.yaml └── default │ ├── manager_config_patch.yaml │ ├── manager_auth_proxy_patch.yaml │ └── kustomization.yaml ├── .dockerignore ├── deployment ├── clusterissuer.tcs.intel.com-sgx-ca.yaml ├── issuer.tcs.intel.com-sgx-ca.yaml ├── certificate-example-naemspaced.yaml ├── certificate-example-clusterscoped.yaml ├── cert-manager-rbac.yaml ├── example-secure-ingress.yaml ├── istio-csr-istio-external-ca-config.yaml ├── crds │ ├── tcs.intel.com_tcsissuers.yaml │ └── tcs.intel.com_tcsclusterissuers.yaml └── tcs_issuer.yaml ├── hack ├── create-pkcs11-config.sh ├── boilerplate.go.txt ├── create-istio-external-ca-secret.sh ├── prepare-release-branch.sh └── create-k8s-csr.sh ├── SECURITY.md ├── .gitignore ├── enclave-config ├── p11Enclave.config.xml └── sign-enclave.sh ├── PROJECT ├── .github └── workflows │ ├── security-scanning.yaml │ └── ci.yml ├── api ├── v1alpha1 │ ├── groupversion_info.go │ ├── tcsclusterissuer_types.go │ ├── tcsissuer_types.go │ └── quoteattestation_types.go └── v1alpha2 │ ├── groupversion_info.go │ ├── zz_generated.deepcopy.go │ └── quoteattestation_types.go ├── test └── utils │ ├── tls.go │ └── ca-provide.go ├── internal ├── config │ └── config.go ├── keyprovider │ └── keyprovider.go ├── sgxutils │ └── sgxutils.go ├── tlsutil │ └── tls-util.go ├── signer │ └── signer.go ├── self-ca │ ├── self-ca_test.go │ └── self-ca.go └── k8sutil │ └── k8sutil.go ├── controllers ├── suite_test.go ├── utils.go ├── certificate_request_controller.go └── csr_controller_test.go ├── go.mod ├── Makefile ├── main.go └── Dockerfile /charts/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | Thank you for installing {{ .Chart.Name }}. 2 | -------------------------------------------------------------------------------- /docs/images/tcs-kubernetes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/trusted-certificate-issuer/HEAD/docs/images/tcs-kubernetes.png -------------------------------------------------------------------------------- /config/manager/.env.secret: -------------------------------------------------------------------------------- 1 | config.json={"userPin": "Spglb2MIwe_eHu2", "soPin": "WIpmBBɷ69Όӥs", "tokenLabel": "SgxOperator"} 2 | 3 | -------------------------------------------------------------------------------- /docs/images/tcs-kubernetes-csr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/trusted-certificate-issuer/HEAD/docs/images/tcs-kubernetes-csr.png -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: issuer-serviceaccount 5 | namespace: system 6 | -------------------------------------------------------------------------------- /docs/images/tcs-kubernetes-certmanager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/trusted-certificate-issuer/HEAD/docs/images/tcs-kubernetes-certmanager.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore all files which are not go type 3 | !**/*.go 4 | !**/*.mod 5 | !**/*.sum 6 | -------------------------------------------------------------------------------- /deployment/clusterissuer.tcs.intel.com-sgx-ca.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tcs.intel.com/v1alpha1 2 | kind: TCSClusterIssuer 3 | metadata: 4 | name: sgx-ca 5 | spec: 6 | secretName: sgx-ca-secret-cluster 7 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: 7 | - "/metrics" 8 | verbs: 9 | - get 10 | -------------------------------------------------------------------------------- /deployment/issuer.tcs.intel.com-sgx-ca.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tcs.intel.com/v1alpha1 2 | kind: TCSIssuer 3 | metadata: 4 | name: sgx-ca 5 | namespace: sandbox 6 | spec: 7 | secretName: sgx-ca 8 | selfSign: true 9 | 10 | -------------------------------------------------------------------------------- /charts/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: tcs-issuer 3 | description: A Helm chart for Trusted Certificate Service for Kubernetes Platform 4 | home: https://github.com/intel/trusted-certificate-issuer 5 | type: application 6 | version: 0.1.0 7 | appVersion: "0.1.0" 8 | -------------------------------------------------------------------------------- /hack/create-pkcs11-config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | random() { 4 | bytes=$(dd if=/dev/random count=1 2>/dev/null | grep -ao "\w" | tr -d '\n' | cut -c1-15) 5 | 6 | echo -n $bytes 7 | } 8 | 9 | echo "userpin=$(random) 10 | sopin=$(random)" > config/manager/.env.secret -------------------------------------------------------------------------------- /charts/values.yaml: -------------------------------------------------------------------------------- 1 | image: 2 | hub: intel 3 | name: trusted-certificate-issuer 4 | tag: "latest" 5 | pullPolicy: Always 6 | 7 | # Any extra arguments for tcs-controller 8 | controllerExtraArgs: {} 9 | #controllerExtraArgs: |- 10 | # - --csr-full-cert-chain=true 11 | # - --use-random-nonce=true 12 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: tcs-issuer 6 | name: metrics-service 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: tcs-issuer 15 | -------------------------------------------------------------------------------- /config/manager/tcs_issuer_config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 2 | kind: ControllerManagerConfig 3 | health: 4 | healthProbeBindAddress: :8083 5 | metrics: 6 | bindAddress: 127.0.0.1:8080 7 | webhook: 8 | port: 9443 9 | leaderElection: 10 | leaderElect: true 11 | resourceName: bb9c3a43.sgx.intel.com 12 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: tcs-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: tcs-issuer-serviceaccount 12 | namespace: tcs-issuer 13 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_quoteattestations.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: quoteattestations.tcs.intel.com 8 | 9 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: tcs-proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: tcs-issuer-serviceaccount 12 | namespace: tcs-issuer 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: tcs-leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: tcs-issuer-serviceaccount 12 | namespace: tcs-issuer 13 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | Intel is committed to rapidly addressing security vulnerabilities affecting our customers and providing clear guidance on the solution, impact, severity and mitigation. 3 | 4 | ## Reporting a Vulnerability 5 | Please report any security vulnerabilities in this project utilizing the guidelines [here](https://www.intel.com/content/www/us/en/security-center/vulnerability-handling-guidelines.html). 6 | -------------------------------------------------------------------------------- /charts/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_quoteattestations.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: quoteattestations.tcs.intel.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | 16 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - tcs_issuer.yaml 3 | 4 | generatorOptions: 5 | disableNameSuffixHash: true 6 | 7 | configMapGenerator: 8 | - files: 9 | - tcs_issuer_config.yaml 10 | name: config 11 | 12 | secretGenerator: 13 | - envs: 14 | - .env.secret 15 | name: issuer-pkcs11-conf 16 | 17 | apiVersion: kustomize.config.k8s.io/v1beta1 18 | kind: Kustomization 19 | images: 20 | - name: tcs-issuer 21 | newName: docker.io/intel/trusted-certificate-issuer 22 | newTag: latest 23 | -------------------------------------------------------------------------------- /deployment/certificate-example-naemspaced.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: Certificate 3 | metadata: 4 | name: sample-cert2-sgx-ca 5 | namespace: sandbox 6 | spec: 7 | secretName: sample-cert 8 | commonName: example.com 9 | isCA: false 10 | privateKey: 11 | algorithm: RSA 12 | encoding: PKCS1 13 | size: 3072 14 | usages: 15 | - server auth 16 | - client auth 17 | dnsNames: 18 | - example.com 19 | issuerRef: 20 | name: sgx-ca 21 | kind: TCSIssuer 22 | group: tcs.intel.com 23 | 24 | -------------------------------------------------------------------------------- /deployment/certificate-example-clusterscoped.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: Certificate 3 | metadata: 4 | name: sample-cert1-sgx-ca 5 | namespace: default 6 | spec: 7 | secretName: sample-cert 8 | commonName: otherexample.com 9 | isCA: false 10 | privateKey: 11 | algorithm: RSA 12 | encoding: PKCS1 13 | size: 3072 14 | usages: 15 | - server auth 16 | - client auth 17 | dnsNames: 18 | - otherexample.com 19 | issuerRef: 20 | name: sgx-ca 21 | kind: TCSClusterIssuer 22 | group: tcs.intel.com 23 | 24 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: operator 11 | args: 12 | - "--config=controller_manager_config.yaml" 13 | volumeMounts: 14 | - name: manager-config 15 | mountPath: /controller_manager_config.yaml 16 | subPath: controller_manager_config.yaml 17 | volumes: 18 | - name: manager-config 19 | configMap: 20 | name: manager-config 21 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | scheme: https 15 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 16 | tlsConfig: 17 | insecureSkipVerify: true 18 | selector: 19 | matchLabels: 20 | control-plane: controller-manager 21 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | 21 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Intel(R). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - coordination.k8s.io 21 | resources: 22 | - leases 23 | verbs: 24 | - get 25 | - list 26 | - watch 27 | - create 28 | - update 29 | - patch 30 | - delete 31 | - apiGroups: 32 | - "" 33 | resources: 34 | - events 35 | verbs: 36 | - create 37 | - patch 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, built with `go test -c` 10 | bin 11 | testbin/* 12 | 13 | # Test binary, build with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Dependency directories (remove the comment below to include it) 20 | # vendor/ 21 | # Kubernetes Generated files - skip generated files, except for vendored files 22 | config/manager/kustomization.yaml 23 | enclave-config/privatekey.pem 24 | 25 | !vendor/**/zz_generated.* 26 | 27 | # editor and IDE paraphernalia 28 | .idea 29 | *.swp 30 | *.swo 31 | *~ 32 | charts/crds 33 | tcs-issuer*.tgz 34 | -------------------------------------------------------------------------------- /hack/create-istio-external-ca-secret.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Exports sgx-ca secrets to istio-system namespace 5 | # 6 | SGX_CA_SECRET_NAME=sgx-ca-signer 7 | SGX_CA_SECRET_NS=sgx-operator 8 | ISTIOD_EXTERNAL_CA_SECRET_NAME=external-ca-cert 9 | ISTIOD_EXTERNAL_CA_SECRET_NS=istio-system 10 | 11 | # First create target namespace 12 | kubectl create namespace ${ISTIOD_EXTERNAL_CA_SECRET_NS} || true 13 | 14 | cat < 16 | 17 | 10001 18 | 1 19 | 0x40000 20 | 0xA00000 21 | 1 22 | 1 23 | 0 24 | 0 25 | 0xFFFFFFFF 26 | 27 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: tcs.intel.com 2 | layout: 3 | - go.kubebuilder.io/v3 4 | projectName: trusted-certificate-issuer 5 | repo: github.com/intel/trusted-certificate-issuer 6 | resources: 7 | - controller: true 8 | domain: k8s.io 9 | group: certificates 10 | kind: CertificateSigningRequest 11 | version: v1 12 | - controller: true 13 | group: cert-manager.io 14 | kind: CertificateRequest 15 | version: v1 16 | - api: 17 | crdVersion: v1alpha1 18 | namespaced: true 19 | controller: true 20 | kind: QuoteAttestation 21 | path: trusted-certificate-issuer/api/v1alpha1 22 | version: v1alpha1 23 | - api: 24 | crdVersion: v1alpha1 25 | namespaced: true 26 | controller: true 27 | domain: tcs.intel.com 28 | kind: TCSIssuer 29 | path: trusted-certificate-issuer/api/v1alpha1 30 | version: v1alpha1 31 | - api: 32 | crdVersion: v1alpha1 33 | controller: true 34 | domain: tcs.intel.com 35 | kind: ClusterIssuer 36 | path: trusted-certificate-issuer/api/v1alpha1 37 | version: v1alpha1 38 | version: "3" 39 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | 2 | # This kustomization.yaml is not intended to be run by itself, 3 | # since it depends on service name and namespace that are out of this kustomize package. 4 | # It should be run by config/default 5 | resources: 6 | - tcs.intel.com_quoteattestations.yaml 7 | - tcs.intel.com_tcsissuers.yaml 8 | - tcs.intel.com_tcsclusterissuers.yaml 9 | #+kubebuilder:scaffold:crdkustomizeresource 10 | 11 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 12 | # patches here are for enabling the conversion webhook for each CRD 13 | #- patches/webhook_in_quoteattestations.yaml 14 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 15 | 16 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 17 | # patches here are for enabling the CA injection for each CRD 18 | #- patches/cainjection_in_quoteattestations.yaml 19 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 20 | 21 | # the following config is for teaching kustomize how to do kustomization for CRDs. 22 | configurations: 23 | - kustomizeconfig.yaml 24 | apiVersion: kustomize.config.k8s.io/v1beta1 25 | kind: Kustomization 26 | -------------------------------------------------------------------------------- /.github/workflows/security-scanning.yaml: -------------------------------------------------------------------------------- 1 | name: Security Scanning 2 | 3 | on: 4 | push: 5 | branches: [ main, 'release-*' ] 6 | tags: [ '*' ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | codeQL-init: 14 | runs-on: ubuntu-latest 15 | environment: 16 | name: dev 17 | url: https://github.com 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@master 22 | 23 | - name: Initialize CodeQL 24 | uses: github/codeql-action/init@v2 25 | with: 26 | languages: go 27 | 28 | - name: Perform CodeQL Analysis 29 | uses: github/codeql-action/analyze@v2 30 | 31 | codeQL-upload: 32 | runs-on: ubuntu-latest 33 | environment: 34 | name: dev 35 | url: https://github.com 36 | 37 | steps: 38 | - name: Checkout 39 | uses: actions/checkout@master 40 | 41 | - name: Upload result to GitHub Code Scanning 42 | if: ${{ github.event_name != 'pull_request' }} 43 | uses: github/codeql-action/upload-sarif@v2 44 | with: 45 | sarif_file: results.sarif 46 | wait-for-processing: true 47 | -------------------------------------------------------------------------------- /docs/azure.md: -------------------------------------------------------------------------------- 1 | # Trusted Certificate Service deployment in Azure 2 | 3 | This document describe the steps how to deploy Trusted Certificate Service (TCS) in Azure. 4 | 5 | ## Prerequisites 6 | 7 | - Install and learn how to use the [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/) 8 | - [Azure account](https://portal.azure.com/) 9 | - One or more Azure DCsv3 VM SKUs in your cluster 10 | 11 | ## Create Kubernetes cluster on Azure 12 | 13 | You would need a Azure Kubernetes cluster (AKS) with at least one confidential computing (SGX) node. To learn more about Azure confidential computing click [here](https://docs.microsoft.com/en-us/azure/confidential-computing/). 14 | 15 | > NOTE: When creating the resource group (`az group create`) ensure the location has [DCsv3](https://docs.microsoft.com/en-us/azure/virtual-machines/dcv3-series) instances. 16 | 17 | Follow the cluster creating istructions [here](https://docs.microsoft.com/en-us/azure/confidential-computing/confidential-enclave-nodes-aks-get-started#create-an-aks-cluster-with-a-system-node-pool). 18 | 19 | ## Deploy Trusted Certificate Service 20 | 21 | Once you have the cluster running in Azure you can deploy TCS normally for example using [Helm](./helm.md). 22 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Intel(R). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the tcs.intel.com v1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=tcs.intel.com 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupName is the group name of the objects 29 | GroupName = "tcs.intel.com" 30 | 31 | // GroupVersion is group version used to register these objects 32 | GroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} 33 | 34 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 35 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 36 | 37 | // AddToScheme adds the types in this group-version to the given scheme. 38 | AddToScheme = SchemeBuilder.AddToScheme 39 | ) 40 | -------------------------------------------------------------------------------- /api/v1alpha2/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Intel(R). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha2 contains API Schema definitions for the tcs.intel.com v1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=tcs.intel.com 20 | package v1alpha2 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupName is the group name of the objects 29 | GroupName = "tcs.intel.com" 30 | 31 | // GroupVersion is group version used to register these objects 32 | GroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha2"} 33 | 34 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 35 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 36 | 37 | // AddToScheme adds the types in this group-version to the given scheme. 38 | AddToScheme = SchemeBuilder.AddToScheme 39 | ) 40 | -------------------------------------------------------------------------------- /enclave-config/sign-enclave.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2022 Intel(R). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | function usage() { 18 | echo "$0 - utility to sign a given file. 19 | -in content to sign 20 | -out file path to save the signed enclve 21 | -keyout path where to write the public key used 22 | -h | -help this help string" 23 | exit 1 24 | } 25 | 26 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 27 | 28 | in= 29 | out= 30 | keyout= 31 | while [ $# -gt 0 ]; do 32 | opt=$1 ; shift 33 | case "$opt" in 34 | -in) in=$1; shift ;; 35 | -out) out=$1; shift ;; 36 | -keyout) keyout=$1; shift ;; 37 | -h | -help) usage ;; 38 | *) echo "Unknown argument $opt" 39 | esac 40 | done 41 | 42 | if [ -z "$in" -o -z "$out" ]; then 43 | echo "Incomplete arguments" 44 | usage 45 | fi 46 | 47 | if [ ! -f ${SCRIPT_DIR}/privatekey.pem ]; then 48 | echo "ERROR: Missing privatekey.pem. You can generate one using 'make enclave-config/priatekey.pem'" 49 | fi 50 | 51 | openssl dgst -sha256 -sign ${SCRIPT_DIR}/privatekey.pem -out $out $in && \ 52 | if [ ! -z "$keyout" ] ; then openssl rsa -in ${SCRIPT_DIR}/privatekey.pem -pubout > $keyout ; fi 53 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CICD 2 | 3 | on: 4 | push: 5 | branches: [ main, 'release-*' ] 6 | tags: [ '*' ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | permissions: read-all 11 | 12 | env: 13 | REGISTRY: ghcr.io 14 | IMG_NAME: ${{ github.repository }} 15 | 16 | jobs: 17 | 18 | build: 19 | runs-on: ubuntu-latest 20 | environment: 21 | name: dev 22 | url: https://github.com 23 | 24 | steps: 25 | - name: Set up env 26 | run: | 27 | if [[ $GITHUB_EVENT_NAME == 'pull_request' ]]; then 28 | echo "IMG_TAG=latest" >> "$GITHUB_ENV" 29 | else 30 | echo "IMG_TAG=$GITHUB_REF_NAME" >> "$GITHUB_ENV" 31 | fi 32 | 33 | - uses: actions/checkout@v2 34 | 35 | - name: Set up Go 36 | uses: actions/setup-go@v2 37 | with: 38 | go-version: '>=1.19' 39 | 40 | # Don't run this since PR does not have access to secrets. 41 | # Let the make docker-build generate the temp. key 42 | - name: Setup signing key 43 | if: ${{ github.event_name != 'pull_request' }} 44 | run: | 45 | echo "$PRIVATE_KEY_PEM" > enclave-config/privatekey.pem 46 | sha256sum enclave-config/privatekey.pem 47 | shell: bash 48 | env: 49 | PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }} 50 | 51 | - name: Login to the container registry 52 | uses: docker/login-action@v2 53 | with: 54 | registry: ${{ env.REGISTRY }} 55 | username: ${{ github.repository_owner }} 56 | password: ${{ secrets.GITHUB_TOKEN }} 57 | 58 | - name: Build 59 | run: make docker-build 60 | env: 61 | IMG_TAG: ${{ env.IMG_TAG }} 62 | 63 | - name: Push 64 | if: ${{ github.event_name != 'pull_request' }} 65 | run: make docker-push 66 | env: 67 | IMG_TAG: ${{ env.IMG_TAG }} 68 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: tcs-issuer 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: tcs- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 16 | # crd/kustomization.yaml 17 | #- ../webhook 18 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 19 | #- ../certmanager 20 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 21 | #- ../prometheus 22 | 23 | # Protect the /metrics endpoint by putting it behind auth. 24 | # If you want your controller-manager to expose the /metrics 25 | # endpoint w/o any authn/z, please comment the following line. 26 | # - manager_auth_proxy_patch.yaml 27 | 28 | # Mount the controller config file for loading manager configurations 29 | # through a ComponentConfig type 30 | #- manager_config_patch.yaml 31 | 32 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 33 | # crd/kustomization.yaml 34 | #- manager_webhook_patch.yaml 35 | 36 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 37 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 38 | # 'CERTMANAGER' needs to be enabled to use ca injection 39 | #- webhookcainjection_patch.yaml 40 | 41 | # the following config is for teaching kustomize how to do var substitution 42 | apiVersion: kustomize.config.k8s.io/v1beta1 43 | kind: Kustomization 44 | resources: 45 | - ../rbac 46 | - ../manager 47 | -------------------------------------------------------------------------------- /api/v1alpha1/tcsclusterissuer_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Intel(R) Corporation. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // +kubebuilder:object:root=true 24 | // +kubebuilder:subresource:status 25 | // +kubebuilder:resource:scope=Cluster 26 | // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=`.metadata.creationTimestamp` 27 | // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=`.status.conditions[?(@.type=='Ready')].status` 28 | // +kubebuilder:printcolumn:name="Reason",type="string",JSONPath=`.status.conditions[?(@.type=='Ready')].reason` 29 | // +kubebuilder:printcolumn:name="Message",type="string",JSONPath=`.status.conditions[?(@.type=='Ready')].message` 30 | // TCSClusterIssuer is the Schema for the clusterissuers API 31 | type TCSClusterIssuer struct { 32 | metav1.TypeMeta `json:",inline"` 33 | metav1.ObjectMeta `json:"metadata,omitempty"` 34 | 35 | Spec TCSIssuerSpec `json:"spec,omitempty"` 36 | Status TCSIssuerStatus `json:"status,omitempty"` 37 | } 38 | 39 | //+kubebuilder:object:root=true 40 | 41 | // TCSClusterIssuerList contains a list of TCSClusterIssuer 42 | type TCSClusterIssuerList struct { 43 | metav1.TypeMeta `json:",inline"` 44 | metav1.ListMeta `json:"metadata,omitempty"` 45 | Items []TCSClusterIssuer `json:"items"` 46 | } 47 | 48 | func init() { 49 | SchemeBuilder.Register(&TCSClusterIssuer{}, &TCSClusterIssuerList{}) 50 | } 51 | -------------------------------------------------------------------------------- /test/utils/tls.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "crypto/tls" 9 | "crypto/x509" 10 | "crypto/x509/pkix" 11 | "encoding/pem" 12 | "math" 13 | "math/big" 14 | "time" 15 | ) 16 | 17 | func NewCACertificate(key crypto.Signer, validFrom time.Time, duration time.Duration, isCA bool) (*x509.Certificate, error) { 18 | max := new(big.Int).SetInt64(math.MaxInt64) 19 | serial, err := rand.Int(rand.Reader, max) 20 | if err != nil { 21 | return nil, err 22 | } 23 | tmpl := &x509.Certificate{ 24 | Version: tls.VersionTLS12, 25 | SerialNumber: serial, 26 | NotBefore: validFrom, 27 | NotAfter: validFrom.Add(duration).UTC(), 28 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 29 | IsCA: isCA, 30 | BasicConstraintsValid: true, 31 | Subject: pkix.Name{ 32 | CommonName: "test root certificate authority", 33 | Organization: []string{"Go test"}, 34 | }, 35 | } 36 | certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | return x509.ParseCertificate(certBytes) 42 | } 43 | 44 | func NewCertificateRequest(key crypto.Signer, subject pkix.Name) ([]byte, error) { 45 | if key == nil { 46 | var err error 47 | key, err = rsa.GenerateKey(rand.Reader, 1024) 48 | if err != nil { 49 | return nil, err 50 | } 51 | } 52 | 53 | var alog x509.SignatureAlgorithm 54 | switch key.(type) { 55 | case *rsa.PrivateKey: 56 | alog = x509.SHA256WithRSA 57 | case *ecdsa.PrivateKey: 58 | alog = x509.ECDSAWithSHA256 59 | } 60 | 61 | template := &x509.CertificateRequest{ 62 | Subject: subject, 63 | SignatureAlgorithm: alog, 64 | } 65 | 66 | return x509.CreateCertificateRequest(rand.Reader, template, key) 67 | } 68 | 69 | func EncodeCSR(csr []byte) []byte { 70 | return pem.EncodeToMemory(&pem.Block{ 71 | Type: "CERTIFICATE REQUEST", 72 | Bytes: csr, 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Intel(R). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "io/ioutil" 23 | 24 | "github.com/creasty/defaults" 25 | ) 26 | 27 | const ( 28 | KeyWrapAesGCM = "aes_gcm" 29 | KeyWrapAesKeyWrapPad = "aes_key_wrap_pad" 30 | ) 31 | 32 | type Config struct { 33 | MetricsAddress string 34 | HealthProbeAddress string 35 | LeaderElection bool 36 | CertManagerIssuer bool 37 | CSRFullCertChain bool 38 | RandomNonce bool 39 | 40 | HSMConfigPath string 41 | HSMConfig HSMConfig 42 | KeyWrapMechanism string 43 | } 44 | 45 | type HSMConfig struct { 46 | TokenLabel string `json:"tokenLabel" default:"SgxOperator"` 47 | UserPin string `json:"userPin"` 48 | SoPin string `json:"soPin"` 49 | } 50 | 51 | func (cfg *Config) Validate() error { 52 | if cfg.HSMConfigPath != "" { 53 | if err := cfg.parseHSMConfig(); err != nil { 54 | return fmt.Errorf("failed to parse hsm config: %v", err) 55 | } 56 | } 57 | if cfg.HSMConfig.SoPin == "" || cfg.HSMConfig.UserPin == "" { 58 | return fmt.Errorf("invalid HSM config: missing user/so pin") 59 | } 60 | 61 | if cfg.KeyWrapMechanism != KeyWrapAesGCM && 62 | cfg.KeyWrapMechanism != KeyWrapAesKeyWrapPad { 63 | return fmt.Errorf("invalid key wrap mechanism '%s'", cfg.KeyWrapMechanism) 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func (cfg *Config) parseHSMConfig() error { 70 | if cfg.HSMConfigPath == "" { 71 | return nil 72 | } 73 | defaults.Set(&cfg.HSMConfig) 74 | 75 | data, err := ioutil.ReadFile(cfg.HSMConfigPath) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | return json.Unmarshal(data, &cfg.HSMConfig) 81 | } 82 | -------------------------------------------------------------------------------- /internal/keyprovider/keyprovider.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Intel(R). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package keyprovider 17 | 18 | import ( 19 | "crypto/rsa" 20 | "crypto/x509" 21 | "errors" 22 | 23 | "github.com/intel/trusted-certificate-issuer/internal/signer" 24 | ) 25 | 26 | var ErrNotFound = errors.New("NotFound") 27 | 28 | type QuoteInfo struct { 29 | Quote []byte 30 | Nonce []byte 31 | PublicKey *rsa.PublicKey 32 | } 33 | 34 | type KeyProvider interface { 35 | // SignerNames lists all the valid signer names, the list 36 | // might also contains the pending signers. 37 | SignerNames() []string 38 | 39 | // AddSigner starts process of initiating a new signer withe given name. 40 | // Returns error if it fails do so. Adding signer is an asynchronous process. 41 | // Use GetSignerForName() to retrieve the initialized signer. 42 | AddSigner(signerName string, selfSign bool) (*signer.Signer, error) 43 | 44 | // RemoveSigner removes the secrets stored for the given signerName. 45 | RemoveSigner(signerName string) error 46 | 47 | // GetSignerForName returns the available signer for give signerName. 48 | // Returns "not found" error if the given signerName is not found in the list. 49 | // Returns any other error occurred while provisioning the CA. 50 | GetSignerForName(signerName string) (*signer.Signer, error) 51 | 52 | // ProvisionSigner stores the given CA key and certificate for the signerName. 53 | // The key must be encrypted with the given publick-key used while quote-generation. 54 | ProvisionSigner(signerName string, encryptedKey []byte, cert *x509.Certificate) (*signer.Signer, error) 55 | 56 | // GetQuote returns the SGX quote generated for the signerName 57 | GetQuote(signerName string) (*QuoteInfo, error) 58 | } 59 | -------------------------------------------------------------------------------- /hack/prepare-release-branch.sh: -------------------------------------------------------------------------------- 1 | # !/bin/bash 2 | # 3 | # Script runs below steps to prepare a new release branch: 4 | # - Fetch the latest main branch from remote origin (it must point to github.com/intel/trusted-certificate-issuer). 5 | # - Create a new branch with current main HEAD. 6 | # - Run needed make targets to generate manifests with the new version. 7 | # - Modify helm charts with new version. 8 | # - Commit the changes. 9 | # - And return back to the previous branch. 10 | # 11 | set -o pipefail 12 | set -o errexit 13 | 14 | SOURCE=$(dirname "$(readlink -f "$0")") 15 | REPO_ROOT=$(dirname $SOURCE) 16 | 17 | VERSION= 18 | pwd=$(pwd) 19 | current_branch=$(git branch --show-current) 20 | release_branch= 21 | 22 | function Usage { 23 | echo "Usage:" 24 | echo " $0 --version " 25 | } 26 | 27 | for opt in $@ 28 | do 29 | case "$opt" in 30 | --version) 31 | shift ; VERSION=$1 ;; 32 | -h | --help) 33 | Usage ; exit ;; 34 | -*) shift ; echo "Unrecognized option $opt" ;; 35 | esac 36 | done 37 | 38 | if [ -z "$VERSION" ]; then 39 | echo "ERROR: No release version set." 40 | Usage 41 | exit 42 | fi 43 | 44 | function Cleanup { 45 | git checkout $current_branch 46 | if [ ! -z "$release_branch" ] ; then 47 | git branch -D "$release_branch" 48 | fi 49 | cd "$pwd" 50 | } 51 | trap Cleanup EXIT 52 | 53 | SHORT_VERSION=$(echo $VERSION | sed -e 's/\([0-9]*\.[0-9]*\)\..*/\1/') 54 | echo "Using release VERSION=$VERSION" 55 | 56 | release_branch="release-$SHORT_VERSION" 57 | cd "$REPO_ROOT" 58 | git fetch origin 59 | git checkout -b $release_branch $(git show --oneline origin/main | head -1 | cut -f1 -d ' ') 60 | make generate deploy-manifests REGISTRY="docker.io" IMG_TAG=$VERSION 61 | sed -i -e "s;\(.*version: \).*;\1$VERSION;g" -e 's;\(.*appVersion: \).*;\1"'$VERSION'";g' ./charts/Chart.yaml 62 | sed -i "s;\(.*tag: \).*;\1$VERSION;g" ./charts/values.yaml 63 | git checkout ./config/manager/kustomization.yaml 64 | git add ./deployment ./charts && git commit -m "Release v$VERSION" 65 | # Unset release_branch so that Cleanup does not delete the branch. 66 | release_branch="" 67 | 68 | echo ======================== 69 | echo "Created new release branch $release_branch. Review and run 'git push origin $release_branch'." 70 | echo ======================= 71 | 72 | -------------------------------------------------------------------------------- /deployment/istio-csr-istio-external-ca-config.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Originally from: 3 | # https://github.com/cert-manager/istio-csr/blob/main/docs/istio-config-getting-started.yaml 4 | # 5 | apiVersion: install.istio.io/v1alpha1 6 | kind: IstioOperator 7 | metadata: 8 | namespace: istio-system 9 | spec: 10 | profile: "demo" 11 | hub: gcr.io/istio-release 12 | meshConfig: 13 | # Change the following line to configure the trust domain of the Istio cluster. 14 | trustDomain: cluster.local 15 | values: 16 | global: 17 | # Change certificate provider to cert-manager istio agent for istio agent 18 | caAddress: cert-manager-istio-csr.cert-manager.svc:443 19 | components: 20 | pilot: 21 | k8s: 22 | env: 23 | # Disable istiod CA Sever functionality 24 | - name: ENABLE_CA_SERVER 25 | value: "false" 26 | overlays: 27 | - apiVersion: apps/v1 28 | kind: Deployment 29 | name: istiod 30 | patches: 31 | 32 | # Mount istiod serving and webhook certificate from Secret mount 33 | - path: spec.template.spec.containers.[name:discovery].args[-1] 34 | value: "--tlsCertFile=/etc/cert-manager/tls/tls.crt" 35 | - path: spec.template.spec.containers.[name:discovery].args[-1] 36 | value: "--tlsKeyFile=/etc/cert-manager/tls/tls.key" 37 | - path: spec.template.spec.containers.[name:discovery].args[-1] 38 | value: "--caCertFile=/etc/cert-manager/ca/root-cert.pem" 39 | 40 | - path: spec.template.spec.containers.[name:discovery].volumeMounts[-1] 41 | value: 42 | name: cert-manager 43 | mountPath: "/etc/cert-manager/tls" 44 | readOnly: true 45 | - path: spec.template.spec.containers.[name:discovery].volumeMounts[-1] 46 | value: 47 | name: ca-root-cert 48 | mountPath: "/etc/cert-manager/ca" 49 | readOnly: true 50 | 51 | - path: spec.template.spec.volumes[-1] 52 | value: 53 | name: cert-manager 54 | secret: 55 | secretName: istiod-tls 56 | - path: spec.template.spec.volumes[-1] 57 | value: 58 | name: ca-root-cert 59 | configMap: 60 | defaultMode: 420 61 | name: istio-ca-root-cert 62 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | creationTimestamp: null 6 | name: role 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - secrets 12 | verbs: 13 | - create 14 | - delete 15 | - get 16 | - list 17 | - patch 18 | - update 19 | - watch 20 | - apiGroups: 21 | - "" 22 | resources: 23 | - secrets/finalizers 24 | verbs: 25 | - get 26 | - patch 27 | - update 28 | - apiGroups: 29 | - cert-manager.io 30 | resources: 31 | - certificaterequests 32 | verbs: 33 | - get 34 | - list 35 | - patch 36 | - update 37 | - watch 38 | - apiGroups: 39 | - cert-manager.io 40 | resources: 41 | - certificaterequests/finalizers 42 | verbs: 43 | - update 44 | - apiGroups: 45 | - cert-manager.io 46 | resources: 47 | - certificaterequests/status 48 | verbs: 49 | - get 50 | - patch 51 | - update 52 | - apiGroups: 53 | - certificates.k8s.io 54 | resources: 55 | - certificatesigningrequests 56 | verbs: 57 | - create 58 | - delete 59 | - get 60 | - list 61 | - patch 62 | - update 63 | - watch 64 | - apiGroups: 65 | - certificates.k8s.io 66 | resources: 67 | - certificatesigningrequests/finalizers 68 | verbs: 69 | - update 70 | - apiGroups: 71 | - certificates.k8s.io 72 | resources: 73 | - certificatesigningrequests/status 74 | verbs: 75 | - get 76 | - patch 77 | - update 78 | - apiGroups: 79 | - certificates.k8s.io 80 | resourceNames: 81 | - tcsclusterissuer.tcs.intel.com/* 82 | - tcsissuer.tcs.intel.com/* 83 | resources: 84 | - signers 85 | verbs: 86 | - sign 87 | - apiGroups: 88 | - tcs.intel.com 89 | resources: 90 | - quoteattestations 91 | verbs: 92 | - create 93 | - delete 94 | - get 95 | - list 96 | - patch 97 | - watch 98 | - apiGroups: 99 | - tcs.intel.com 100 | resources: 101 | - quoteattestations/finalizers 102 | verbs: 103 | - update 104 | - apiGroups: 105 | - tcs.intel.com 106 | resources: 107 | - quoteattestations/status 108 | verbs: 109 | - get 110 | - patch 111 | - update 112 | - apiGroups: 113 | - tcs.intel.com 114 | resources: 115 | - tcsclusterissuers 116 | - tcsissuers 117 | verbs: 118 | - get 119 | - list 120 | - patch 121 | - update 122 | - watch 123 | - apiGroups: 124 | - tcs.intel.com 125 | resources: 126 | - tcsclusterissuers/status 127 | - tcsissuers/status 128 | verbs: 129 | - get 130 | - patch 131 | - update 132 | -------------------------------------------------------------------------------- /hack/create-k8s-csr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function generate_key() { 4 | echo "Generating private key ..." 5 | openssl genrsa -out $1 2048 6 | } 7 | 8 | function generate_csr() { 9 | key_file=$1 10 | csr_file=$2 11 | echo "Generating signing request ..." 12 | openssl req -new -key $key_file -out $csr_file -subj "/O=Foo Comapny/CN=foo.bar.com" 13 | } 14 | 15 | function create_k8s_csr() { 16 | csr=$1 17 | name=$2 18 | namespace=$3 19 | signer=$4 20 | 21 | echo "Generating K8s CSR object ..." 22 | set -x 23 | cat < Private key file to sign. Created one if not provided. 44 | -c|--csr-file CSR holding the signing request use. 45 | -n|--name Name of the CSR object to be created. 46 | Defaults to "sgx-test-csr" 47 | -s|--namespace Namespace of the CSR object to be created. 48 | Uses "default" namespace not provided. 49 | --signer Name of the signer. Default "intel.com/sgx" 50 | -h|--help Display this help and exit 51 | 52 | " 2>&2 53 | } 54 | 55 | key_file="" 56 | csr_file="" 57 | csr_name="sgx-test-csr" 58 | csr_ns="default" 59 | signer="intel.com/sgx" 60 | 61 | while [ $# -gt 0 ]; do 62 | 63 | case "$1" in 64 | -k|--key-file) 65 | key_file=$2; shift; shift 66 | ;; 67 | 68 | -c|--csr-file) 69 | csr_file=$2; shift; shift 70 | ;; 71 | 72 | -n|--name) 73 | csr_name=$2; shift; shift 74 | ;; 75 | 76 | -s|--namespace) 77 | csr_ns=$2; shift; shift 78 | ;; 79 | 80 | --signer) 81 | signer=$2; shift; shift 82 | ;; 83 | 84 | -h|--help) 85 | Usage ; exit 0 ;; 86 | 87 | *) echo "Unknown option: $1" 2>&2 ; Usage 88 | exit 1 ;; 89 | esac 90 | 91 | done 92 | 93 | if [ "$csr_file" = "" ]; then 94 | if [ "$key_file" = "" ]; then 95 | key_file=/tmp/sgx.key 96 | generate_key "$key_file" 97 | fi 98 | csr_file="/tmp/sgx.csr" 99 | generate_csr "$key_file" "$csr_file" 100 | fi 101 | 102 | create_k8s_csr "$csr_file" "$csr_name" "$csr_ns" "$signer" && echo "Done" -------------------------------------------------------------------------------- /internal/sgxutils/sgxutils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Intel(R) 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package sgxutils 7 | 8 | /* 9 | typedef unsigned long int CK_ULONG; 10 | typedef void * CK_BYTE_PTR; 11 | typedef struct CK_RSA_PUBLIC_KEY_PARAMS { 12 | CK_ULONG ulExponentLen; 13 | CK_ULONG ulModulusLen; 14 | } CK_RSA_PUBLIC_KEY_PARAMS; 15 | 16 | CK_ULONG quote_offset(CK_BYTE_PTR bytes) { 17 | CK_RSA_PUBLIC_KEY_PARAMS* params = (CK_RSA_PUBLIC_KEY_PARAMS*)bytes; 18 | if (params == NULL) { 19 | return 0; 20 | } 21 | CK_ULONG pubKeySize = params->ulModulusLen + params->ulExponentLen; 22 | // check for overflow 23 | if (pubKeySize < params->ulModulusLen || pubKeySize < params->ulExponentLen) { 24 | return 0; 25 | } 26 | CK_ULONG offset = sizeof(CK_RSA_PUBLIC_KEY_PARAMS) + pubKeySize; 27 | 28 | return offset; 29 | } 30 | 31 | CK_ULONG rsa_key_params_size() { 32 | return (CK_ULONG)sizeof(CK_RSA_PUBLIC_KEY_PARAMS); 33 | } 34 | 35 | CK_ULONG ulExponentLen_offset(CK_BYTE_PTR bytes) { 36 | CK_RSA_PUBLIC_KEY_PARAMS* params = (CK_RSA_PUBLIC_KEY_PARAMS*)bytes; 37 | if (params == NULL) { 38 | return 0; 39 | } 40 | return params->ulExponentLen; 41 | } 42 | */ 43 | import "C" 44 | import ( 45 | "crypto/rsa" 46 | "fmt" 47 | "math" 48 | "math/big" 49 | "unsafe" 50 | ) 51 | 52 | // ParseQuotePublickey reconstruct the rsa public key 53 | // from received bytes, received bytes structure like this: 54 | // pubkey_params | ulExponentLen | ulModulusLen 55 | // need to slice ulExponentLen and ulModulusLen to 56 | // reconstruct pubkey according to the size of each item 57 | func ParseQuotePublickey(pubkey []byte) (*rsa.PublicKey, error) { 58 | paramsSize := uint64(C.rsa_key_params_size()) 59 | exponentLen := uint64(C.ulExponentLen_offset(*(*C.CK_BYTE_PTR)(unsafe.Pointer(&pubkey)))) 60 | modulusOffset := paramsSize + exponentLen 61 | if modulusOffset >= uint64(len(pubkey)) { 62 | return nil, fmt.Errorf("malformed quote public key: out of bounds") 63 | } 64 | 65 | var bigExponent = new(big.Int) 66 | bigExponent.SetBytes(pubkey[paramsSize:modulusOffset]) 67 | if bigExponent.BitLen() > 32 || bigExponent.Sign() < 1 { 68 | return nil, fmt.Errorf("malformed quote public key") 69 | } 70 | if bigExponent.Uint64() > uint64(math.MaxInt) { 71 | return nil, fmt.Errorf("malformed quote public key: possible data loss in exponent value") 72 | } 73 | exponent := int(bigExponent.Uint64()) 74 | var modulus = new(big.Int) 75 | modulus.SetBytes(pubkey[modulusOffset:]) 76 | return &rsa.PublicKey{ 77 | N: modulus, 78 | E: exponent, 79 | }, nil 80 | } 81 | 82 | // QuoteOffset returns the offset of SGX quote in the 83 | // given quotePublicKey bytes returns by the CTK while 84 | // generating the quote. 85 | func QuoteOffset(quotePublicKey []byte) uint64 { 86 | return uint64(C.quote_offset(*(*C.CK_BYTE_PTR)(unsafe.Pointer("ePublicKey)))) 87 | } 88 | -------------------------------------------------------------------------------- /internal/tlsutil/tls-util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Intel(R) 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package tlsutil 7 | 8 | import ( 9 | "encoding/pem" 10 | "errors" 11 | "fmt" 12 | "runtime" 13 | 14 | "crypto/rsa" 15 | "crypto/x509" 16 | ) 17 | 18 | // EncodeKey returns PEM encoding of give private key 19 | func EncodeKey(key *rsa.PrivateKey) []byte { 20 | if key == nil { 21 | return []byte{} 22 | } 23 | return pem.EncodeToMemory(&pem.Block{ 24 | Type: "RSA PRIVATE KEY", 25 | Bytes: x509.MarshalPKCS1PrivateKey(key), 26 | }) 27 | } 28 | 29 | // EncodePublicKey returns PEM encoding of given public key 30 | func EncodePublicKey(key interface{}) ([]byte, error) { 31 | if key == nil { 32 | return []byte{}, nil 33 | } 34 | bytes, err := x509.MarshalPKIXPublicKey(key) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return pem.EncodeToMemory(&pem.Block{ 40 | Type: "PUBLIC KEY", 41 | Bytes: bytes, 42 | }), nil 43 | } 44 | 45 | // DecodeKey returns the decoded private key of given encodedKey 46 | func DecodeKey(encodedKey []byte) (*rsa.PrivateKey, error) { 47 | block, _ := pem.Decode(encodedKey) 48 | 49 | key, err := x509.ParsePKCS1PrivateKey(block.Bytes) 50 | wipe(block.Bytes) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | runtime.SetFinalizer(key, func(k *rsa.PrivateKey) { 56 | // Zero key after usage 57 | *k = rsa.PrivateKey{} 58 | }) 59 | 60 | return key, nil 61 | } 62 | 63 | // EncodeCert returns PEM encoding of given cert 64 | func EncodeCert(cert *x509.Certificate) []byte { 65 | if cert == nil { 66 | return []byte{} 67 | } 68 | return pem.EncodeToMemory(&pem.Block{ 69 | Type: "CERTIFICATE", 70 | Bytes: cert.Raw, 71 | }) 72 | } 73 | 74 | // DecodeCert return the decoded certificate of given encodedCert 75 | func DecodeCert(pemCert []byte) (*x509.Certificate, error) { 76 | block, rest := pem.Decode(pemCert) 77 | if len(rest) != 0 { 78 | return nil, fmt.Errorf("malformed PEM certificate") 79 | } 80 | 81 | cert, err := x509.ParseCertificate(block.Bytes) 82 | if err != nil { 83 | return nil, err 84 | } 85 | runtime.SetFinalizer(cert, func(c *x509.Certificate) { 86 | wipe(c.Raw) 87 | *c = x509.Certificate{} 88 | }) 89 | 90 | return cert, nil 91 | } 92 | 93 | // DecodeCert return the decoded csr of given encodedCertRequest 94 | func DecodeCertRequest(encodedCertRequest []byte) (*x509.CertificateRequest, error) { 95 | block, _ := pem.Decode(encodedCertRequest) 96 | if block == nil || block.Type != "CERTIFICATE REQUEST" { 97 | return nil, errors.New("PEM block is not a CERTIFICATE REQUEST") 98 | } 99 | csr, err := x509.ParseCertificateRequest(block.Bytes) 100 | if err != nil { 101 | return nil, err 102 | } 103 | return csr, nil 104 | } 105 | 106 | func wipe(arr []byte) { 107 | for i := range arr { 108 | arr[i] = 0 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /internal/signer/signer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Intel(R) 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package signer 7 | 8 | import ( 9 | "crypto" 10 | "crypto/x509" 11 | "sync" 12 | ) 13 | 14 | type SignerState string 15 | 16 | const ( 17 | // New signer whose secrets are not available 18 | // with the operator. 19 | new SignerState = "New" 20 | // Ready represents the signer secrets are available and 21 | // is ready serving. 22 | ready SignerState = "Ready" 23 | // Failed represents some error has occurred while initializing 24 | // the signer. 25 | failed SignerState = "Failed" 26 | ) 27 | 28 | type attestationRequest struct { 29 | name, namespace string 30 | } 31 | 32 | type Signer struct { 33 | crypto.Signer 34 | cert *x509.Certificate 35 | name string 36 | state SignerState 37 | err error 38 | req attestationRequest 39 | } 40 | 41 | func NewSigner(name string) *Signer { 42 | return &Signer{name: name, state: new} 43 | } 44 | 45 | func (s Signer) Name() string { 46 | return s.name 47 | } 48 | 49 | func (s Signer) NotInitialized() bool { 50 | return s.state == new 51 | } 52 | 53 | func (s Signer) Ready() bool { 54 | return s.state == ready 55 | } 56 | 57 | func (s Signer) Failed() (bool, error) { 58 | if s.state == failed { 59 | return true, s.err 60 | } 61 | return false, nil 62 | } 63 | 64 | func (s Signer) Error() error { 65 | return s.err 66 | } 67 | 68 | func (s Signer) Certificate() *x509.Certificate { 69 | if s.state == ready { 70 | return s.cert 71 | } 72 | return nil 73 | } 74 | 75 | func (s *Signer) SetError(err error) { 76 | if s.state != failed { 77 | s.state = failed 78 | s.err = err 79 | } 80 | } 81 | 82 | func (s *Signer) SetReady(cs crypto.Signer, cert *x509.Certificate) { 83 | s.state = ready 84 | s.Signer = cs 85 | s.cert = cert 86 | s.err = nil 87 | s.req = attestationRequest{} 88 | } 89 | 90 | type SignerMap struct { 91 | signers map[string]*Signer 92 | lock sync.RWMutex 93 | } 94 | 95 | func NewSignerMap() *SignerMap { 96 | return &SignerMap{ 97 | signers: map[string]*Signer{}, 98 | lock: sync.RWMutex{}, 99 | } 100 | } 101 | 102 | func (sm *SignerMap) Names() []string { 103 | sm.lock.RLock() 104 | defer sm.lock.RUnlock() 105 | names := []string{} 106 | for name, _ := range sm.signers { 107 | names = append(names, name) 108 | } 109 | return names 110 | } 111 | 112 | func (sm *SignerMap) Get(name string) *Signer { 113 | sm.lock.RLock() 114 | defer sm.lock.RUnlock() 115 | 116 | return sm.signers[name] 117 | } 118 | 119 | func (sm *SignerMap) UnInitializedSigners() []*Signer { 120 | sm.lock.RLock() 121 | defer sm.lock.RUnlock() 122 | 123 | uninitialized := []*Signer{} 124 | for _, s := range sm.signers { 125 | if s.NotInitialized() { 126 | uninitialized = append(uninitialized, s) 127 | } 128 | } 129 | return uninitialized 130 | } 131 | 132 | func (sm *SignerMap) Add(s *Signer) { 133 | sm.lock.Lock() 134 | defer sm.lock.Unlock() 135 | 136 | if _, ok := sm.signers[s.name]; !ok { 137 | sm.signers[s.name] = s 138 | } 139 | } 140 | 141 | func (sm *SignerMap) Delete(s *Signer) { 142 | sm.lock.Lock() 143 | defer sm.lock.Unlock() 144 | 145 | delete(sm.signers, s.name) 146 | } 147 | -------------------------------------------------------------------------------- /test/utils/ca-provide.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "errors" 8 | "fmt" 9 | "strings" 10 | "time" 11 | 12 | "github.com/intel/trusted-certificate-issuer/internal/keyprovider" 13 | "github.com/intel/trusted-certificate-issuer/internal/signer" 14 | "github.com/intel/trusted-certificate-issuer/internal/tlsutil" 15 | ) 16 | 17 | const certificateDuration = time.Hour * 24 18 | 19 | type SignerError struct { 20 | Name string 21 | ErrMessage string 22 | } 23 | type Config struct { 24 | KnownSigners []string 25 | AddSignerError SignerError 26 | ProvisionSignerError SignerError 27 | } 28 | 29 | type fakeKeyProvider struct { 30 | signers map[string]*signer.Signer 31 | cfg Config 32 | } 33 | 34 | var _ keyprovider.KeyProvider = &fakeKeyProvider{} 35 | 36 | func NewKeyProvider(cfg Config) keyprovider.KeyProvider { 37 | signers := map[string]*signer.Signer{} 38 | for _, name := range cfg.KnownSigners { 39 | signers[name] = signer.NewSigner(name) 40 | } 41 | return &fakeKeyProvider{ 42 | signers: signers, 43 | cfg: cfg, 44 | } 45 | } 46 | 47 | func (kp *fakeKeyProvider) SignerNames() []string { 48 | names := []string{} 49 | for s := range kp.signers { 50 | names = append(names, s) 51 | } 52 | 53 | return names 54 | } 55 | 56 | func (kp *fakeKeyProvider) AddSigner(name string, selfSign bool) (*signer.Signer, error) { 57 | if kp.cfg.AddSignerError.Name != "" && strings.HasSuffix(name, kp.cfg.AddSignerError.Name) { 58 | return nil, errors.New(kp.cfg.AddSignerError.ErrMessage) 59 | } 60 | if s, ok := kp.signers[name]; ok { 61 | return s, nil 62 | } 63 | 64 | s := signer.NewSigner(name) 65 | key, err := rsa.GenerateKey(rand.Reader, 3072) 66 | if err != nil { 67 | return nil, err 68 | } 69 | cert, err := NewCACertificate(key, time.Now(), certificateDuration, true) 70 | if err != nil { 71 | return nil, err 72 | } 73 | s.SetReady(key, cert) 74 | kp.signers[name] = s 75 | return s, nil 76 | } 77 | 78 | func (kp *fakeKeyProvider) RemoveSigner(name string) error { 79 | if _, ok := kp.signers[name]; ok { 80 | kp.signers[name] = nil 81 | delete(kp.signers, name) 82 | } 83 | return nil 84 | } 85 | 86 | func (kp *fakeKeyProvider) GetSignerForName(signerName string) (*signer.Signer, error) { 87 | s, ok := kp.signers[signerName] 88 | if !ok { 89 | return nil, keyprovider.ErrNotFound 90 | } 91 | 92 | return s, nil 93 | } 94 | 95 | func (kp *fakeKeyProvider) ProvisionSigner(signerName string, base64Key []byte, cert *x509.Certificate) (*signer.Signer, error) { 96 | if kp.cfg.ProvisionSignerError.Name != "" && strings.HasSuffix(signerName, kp.cfg.ProvisionSignerError.Name) { 97 | return nil, errors.New(kp.cfg.ProvisionSignerError.ErrMessage) 98 | } 99 | s := signer.NewSigner(signerName) 100 | key, err := tlsutil.DecodeKey(base64Key) 101 | if err != nil { 102 | return nil, fmt.Errorf("corrupted key data: %v", err) 103 | } 104 | 105 | s.SetReady(key, cert) 106 | kp.signers[signerName] = s 107 | return s, nil 108 | } 109 | 110 | func (kp *fakeKeyProvider) GetQuote(string) (*keyprovider.QuoteInfo, error) { 111 | key, err := rsa.GenerateKey(rand.Reader, 2048) 112 | if err != nil { 113 | return nil, err 114 | } 115 | return &keyprovider.QuoteInfo{ 116 | Quote: []byte("DummyQuote"), 117 | PublicKey: &key.PublicKey, 118 | Nonce: []byte{}, 119 | }, nil 120 | } 121 | -------------------------------------------------------------------------------- /docs/quote-attestation-api.md: -------------------------------------------------------------------------------- 1 | # SGX Quote Attestation 2 | 4 | 5 | 6 | - [Overview](#overview) 7 | - [QuoteAttestation CRD](#quoteattestation-crd) 8 | - [QuoteAttestationSpec](#quoteattestationspec) 9 | - [QuoteAttestationStatus](#quoteattestationstatus) 10 | - [QuoteAttestationCondition](#quoteattestationcondition) 11 | - [QuoteAttestationSecret](#quoteattestationsecret) 12 | 13 | 14 | 15 | ## Overview 16 | 17 | This document describes Trusted Certificate Service (TCS) API used for integrating with external key services to securely provision the certificate authority private key and certificate. 18 | 19 | ## QuoteAttestation CRD 20 | 21 | The `QuoteAttestation` is a namespace-scoped Kubernetes resource in the `sgx.intel.com` API group. The sgx-operator creates an object of this resource in the same namespace in which the operator is running. 22 | 23 | The current API for `QuoteAttestation` resource is: 24 | 25 | | Field | Type | Description | 26 | |---|---|---| 27 | | apiVersion | string | API version in the form of '_group/version_': sgx.intel.com/v1alpha1 | 28 | | metadata | ObjectMeta | Object metadata such as name, namespace etc., | 29 | | spec | QuoteAttestationSpec | Desired state of the object. | 30 | | status | QuoteAttestationStatus | Current attestation status which supposed to be updated by the key-server/attestation-controller | 31 | 32 | ### QuoteAttestationSpec 33 | 34 | The `QuoteAttestationSpec` defined the specification of quote attestation and contains below fields and all of its values are immutable. 35 | 36 | | Field | Type | Description | 37 | |---|---|---| 38 | | quote | []byte|Base64 encoded SGX quote of the enclave | 39 | | quoteVersion | string | Currently only supported value is _ECDSA Quote 3_. | 40 | | serviceId | string| Unique identifier that represents service which is requesting the secret. | 41 | | publicKey | []byte| Key must be used by the key server to encrypt the CA private key. | 42 | | signerNames | []string | List of Kubernetes signer names needs provisioning. | 43 | 44 | ### QuoteAttestationStatus 45 | 46 | A QuoteAttestation's status fields a `QuoteAttestationStatus` object, which carries the detailed state of the attestation request. It is comprised of attestation condition and the list of secrets. 47 | 48 | | Field | Type | Description | 49 | |---|---|---| 50 | | condition | QuoteAttestationCondition | Current status condition of the attestation process. | 51 | | secrets | map[string]QuoteAttestationSecret | The list of provisioned secrets for the given signerNames in the attestation request. | 52 | 53 | #### QuoteAttestationCondition 54 | 55 | | Field | Type | Description | 56 | |---|---|---| 57 | | type | ConditionType | Represents the status of the attestation process., one of `Success` or `Failure`. | 58 | | state | string | A brief machine-friendly reason code(using TitleCase). | 59 | | message | string | A detailed message of the failure for human consumption. | 60 | | LastUpdatedTime | time | Timestamp when the status condition updated. | 61 | 62 | #### QuoteAttestationSecret 63 | 64 | | Field | Type | Description | 65 | |---|---|---| 66 | | secretName | string | Name of the [Kubernetes secret](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/secret-v1/) object that holds the encrypted CA privatekey and certificate. | 67 | | secretType | string | 68 | -------------------------------------------------------------------------------- /config/manager/tcs_issuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: tcs-issuer 6 | name: tcs-issuer 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: controller 12 | namespace: system 13 | labels: 14 | control-plane: tcs-issuer 15 | spec: 16 | selector: 17 | matchLabels: 18 | control-plane: tcs-issuer 19 | replicas: 1 20 | template: 21 | metadata: 22 | labels: 23 | control-plane: tcs-issuer 24 | annotations: 25 | sgx.intel.com/quote-provider: aesmd 26 | spec: 27 | initContainers: 28 | - name: init 29 | image: busybox:1.34.1 #latest stable version 30 | imagePullPolicy: IfNotPresent 31 | ## Set appropriate permissions to tokens directory. 32 | ## The tcs-issuer container runs with UID 5000(tcs-issuer username). 33 | command: ["/bin/chown", "5000:5000", "/home/tcs-issuer/tokens"] 34 | securityContext: 35 | allowPrivilegeEscalation: false 36 | readOnlyRootFilesystem: true 37 | capabilities: 38 | drop: [ "ALL" ] 39 | add: [ "CAP_CHOWN" ] 40 | volumeMounts: 41 | - mountPath: /home/tcs-issuer/tokens 42 | name: tokens-dir 43 | containers: 44 | - command: 45 | - /tcs-issuer 46 | args: 47 | - --leader-elect 48 | - --zap-devel 49 | - --zap-log-level=5 50 | - --metrics-bind-address=:8082 51 | - --health-probe-bind-address=:8083 52 | - --hsm-config-file=/etc/hsm/config.json 53 | - --user-pin=$USER_PIN 54 | - --so-pin=$SO_PIN 55 | - --use-random-nonce=true # Disable this if using KMRA version < 2.2 56 | image: tcs-issuer:0.0.0 57 | imagePullPolicy: Always 58 | name: tcs-issuer 59 | securityContext: 60 | allowPrivilegeEscalation: false 61 | readOnlyRootFilesystem: true 62 | runAsNonRoot: true 63 | capabilities: 64 | drop: [ "ALL" ] 65 | livenessProbe: 66 | httpGet: 67 | path: /healthz 68 | port: 8083 69 | initialDelaySeconds: 10 70 | periodSeconds: 180 71 | readinessProbe: 72 | httpGet: 73 | path: /readyz 74 | port: 8083 75 | initialDelaySeconds: 10 76 | periodSeconds: 5 77 | resources: 78 | limits: 79 | cpu: 500m 80 | memory: 100Mi 81 | sgx.intel.com/enclave: 1 82 | sgx.intel.com/epc: 512Ki 83 | requests: 84 | cpu: 100m 85 | memory: 20Mi 86 | sgx.intel.com/enclave: 1 87 | sgx.intel.com/epc: 512Ki 88 | volumeMounts: 89 | # This is the path expected/configured by the crypto-api-toolkit 90 | # for (un)sealing the tokens. Do not change this path. 91 | - mountPath: /home/tcs-issuer/tokens 92 | name: tokens-dir 93 | - mountPath: /etc/hsm 94 | name: hsm-config 95 | serviceAccountName: tcs-issuer-serviceaccount 96 | terminationGracePeriodSeconds: 10 97 | volumes: 98 | - hostPath: 99 | path: /var/lib/tcs-issuer/tokens 100 | type: DirectoryOrCreate 101 | name: tokens-dir 102 | - secret: 103 | secretName: tcs-issuer-pkcs11-conf 104 | optional: false 105 | name: hsm-config 106 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Intel(R). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers_test 18 | 19 | import ( 20 | "context" 21 | "path/filepath" 22 | "testing" 23 | 24 | . "github.com/onsi/ginkgo" 25 | . "github.com/onsi/gomega" 26 | corev1 "k8s.io/api/core/v1" 27 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | "k8s.io/apimachinery/pkg/runtime" 29 | "k8s.io/client-go/rest" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | "sigs.k8s.io/controller-runtime/pkg/envtest" 32 | logf "sigs.k8s.io/controller-runtime/pkg/log" 33 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 34 | 35 | tcsapi "github.com/intel/trusted-certificate-issuer/api/v1alpha1" 36 | "github.com/intel/trusted-certificate-issuer/api/v1alpha2" 37 | csrv1 "k8s.io/api/certificates/v1" 38 | //+kubebuilder:scaffold:imports 39 | ) 40 | 41 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 42 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 43 | 44 | var cfg *rest.Config 45 | var k8sClient client.Client 46 | var testEnv *envtest.Environment 47 | var scheme *runtime.Scheme 48 | 49 | const ( 50 | testIssuerNS = "test-issuer" 51 | ) 52 | 53 | func TestAPIs(t *testing.T) { 54 | RegisterFailHandler(Fail) 55 | RunSpecs(t, "Controller Suite") 56 | } 57 | 58 | var _ = BeforeSuite(func() { 59 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 60 | 61 | By("bootstrapping test environment") 62 | testEnv = &envtest.Environment{ 63 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd")}, 64 | ErrorIfCRDPathMissing: true, 65 | } 66 | 67 | var err error 68 | 69 | cfg, err = testEnv.Start() 70 | Expect(err).NotTo(HaveOccurred()) 71 | Expect(cfg).NotTo(BeNil()) 72 | 73 | scheme = runtime.NewScheme() 74 | err = csrv1.AddToScheme(scheme) 75 | Expect(err).NotTo(HaveOccurred(), "failed to add Certificate api types to scheme") 76 | err = corev1.AddToScheme(scheme) 77 | Expect(err).NotTo(HaveOccurred(), "failed to add core types to scheme") 78 | err = tcsapi.AddToScheme(scheme) 79 | Expect(err).NotTo(HaveOccurred(), "failed to add TCS types to scheme") 80 | err = v1alpha2.AddToScheme(scheme) 81 | Expect(err).NotTo(HaveOccurred(), "failed to add TCS v1alpha2 types to scheme") 82 | 83 | //+kubebuilder:scaffold:scheme 84 | 85 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) 86 | Expect(err).NotTo(HaveOccurred()) 87 | Expect(k8sClient).NotTo(BeNil()) 88 | 89 | nsObj := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ 90 | Name: testIssuerNS, 91 | }} 92 | err = k8sClient.Create(context.TODO(), nsObj) 93 | Expect(err).ShouldNot(HaveOccurred(), "create issuer namespace") 94 | }, 60) 95 | 96 | var _ = AfterSuite(func() { 97 | if k8sClient != nil { 98 | nsObj := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ 99 | Name: testIssuerNS, 100 | }} 101 | k8sClient.Delete(context.TODO(), nsObj) 102 | } 103 | 104 | By("tearing down the test environment") 105 | err := testEnv.Stop() 106 | Expect(err).NotTo(HaveOccurred()) 107 | }) 108 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/intel/trusted-certificate-issuer 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/ThalesIgnite/crypto11 v1.2.5 7 | github.com/creasty/defaults v1.7.0 8 | github.com/go-logr/logr v1.2.3 9 | github.com/jetstack/cert-manager v1.7.3 10 | github.com/miekg/pkcs11 v1.1.1 11 | github.com/onsi/ginkgo v1.16.5 12 | github.com/onsi/gomega v1.24.1 13 | github.com/stretchr/testify v1.8.1 14 | k8s.io/api v0.26.3 15 | k8s.io/apimachinery v0.26.3 16 | k8s.io/client-go v0.26.3 17 | k8s.io/klog/v2 v2.90.1 18 | sigs.k8s.io/controller-runtime v0.14.5 19 | ) 20 | 21 | require ( 22 | github.com/beorn7/perks v1.0.1 // indirect 23 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/emicklei/go-restful/v3 v3.10.2 // indirect 26 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 27 | github.com/evanphx/json-patch/v5 v5.6.0 // indirect 28 | github.com/fsnotify/fsnotify v1.6.0 // indirect 29 | github.com/go-logr/zapr v1.2.3 // indirect 30 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 31 | github.com/go-openapi/jsonreference v0.20.2 // indirect 32 | github.com/go-openapi/swag v0.22.3 // indirect 33 | github.com/gogo/protobuf v1.3.2 // indirect 34 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 35 | github.com/golang/protobuf v1.5.3 // indirect 36 | github.com/google/gnostic v0.6.9 // indirect 37 | github.com/google/go-cmp v0.5.9 // indirect 38 | github.com/google/gofuzz v1.2.0 // indirect 39 | github.com/google/uuid v1.3.0 // indirect 40 | github.com/imdario/mergo v0.3.14 // indirect 41 | github.com/josharian/intern v1.0.0 // indirect 42 | github.com/json-iterator/go v1.1.12 // indirect 43 | github.com/mailru/easyjson v0.7.7 // indirect 44 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 46 | github.com/modern-go/reflect2 v1.0.2 // indirect 47 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 48 | github.com/nxadm/tail v1.4.8 // indirect 49 | github.com/pkg/errors v0.9.1 // indirect 50 | github.com/pmezard/go-difflib v1.0.0 // indirect 51 | github.com/prometheus/client_golang v1.14.0 // indirect 52 | github.com/prometheus/client_model v0.3.0 // indirect 53 | github.com/prometheus/common v0.42.0 // indirect 54 | github.com/prometheus/procfs v0.9.0 // indirect 55 | github.com/spf13/pflag v1.0.5 // indirect 56 | github.com/thales-e-security/pool v0.0.2 // indirect 57 | go.uber.org/atomic v1.10.0 // indirect 58 | go.uber.org/multierr v1.10.0 // indirect 59 | go.uber.org/zap v1.24.0 // indirect 60 | golang.org/x/net v0.23.0 // indirect 61 | golang.org/x/oauth2 v0.6.0 // indirect 62 | golang.org/x/sys v0.18.0 // indirect 63 | golang.org/x/term v0.18.0 // indirect 64 | golang.org/x/text v0.14.0 // indirect 65 | golang.org/x/time v0.3.0 // indirect 66 | gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect 67 | google.golang.org/appengine v1.6.7 // indirect 68 | google.golang.org/protobuf v1.33.0 // indirect 69 | gopkg.in/inf.v0 v0.9.1 // indirect 70 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 71 | gopkg.in/yaml.v2 v2.4.0 // indirect 72 | gopkg.in/yaml.v3 v3.0.1 // indirect 73 | k8s.io/apiextensions-apiserver v0.26.3 // indirect 74 | k8s.io/component-base v0.26.3 // indirect 75 | k8s.io/kube-aggregator v0.26.3 // indirect 76 | k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a // indirect 77 | k8s.io/utils v0.0.0-20230313181309-38a27ef9d749 // indirect 78 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 79 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 80 | sigs.k8s.io/yaml v1.3.0 // indirect 81 | ) 82 | -------------------------------------------------------------------------------- /docs/istio-csr-external-ca-setup.md: -------------------------------------------------------------------------------- 1 | # Istio integration with cert-manager istio-csr 2 | 3 | cert-manager supports [istio-csr](https://github.com/cert-manager/istio-csr/blob/main/docs/getting_started.md) 4 | which is an agent that allows for Istio workload and control plane components to be secured. 5 | This example shows how to provision Istio workload 6 | certificates using an Issuer provided by the Trusted Certificate Service (TCS). 7 | 8 | 9 | ## Prerequisites for running this example: 10 | 11 | - istioctl 12 | - Helm 13 | - Kubernetes cluster with at least one Intel SGX enabled node 14 | 15 | ## Deployment 16 | 17 | Install `cert-manager` using Helm. You can use any other mechanism for installation as long as the end result is the same. 18 | 19 | ```sh 20 | # Helm setup 21 | helm repo add jetstack https://charts.jetstack.io 22 | helm repo update 23 | 24 | # cert-manager install 25 | helm install cert-manager jetstack/cert-manager --version 1.6.1 --namespace cert-manager --create-namespace --set installCRDs=true 26 | ``` 27 | 28 | Deploy TCS and custom resource definitions (CRDs). 29 | 30 | ```sh 31 | kubectl apply -f deployment/crds/ 32 | kubectl apply -f deployment/tcs_issuer.yaml 33 | ``` 34 | 35 | Create a TCS Issuer that could sign certificates for `istio-system` namespace. We also create the `istio-system` namespace since that is where the certificates will be placed. 36 | 37 | ```sh 38 | kubectl create namespace istio-system 39 | cat << EOF | kubectl create -f - 40 | apiVersion: tcs.intel.com/v1alpha1 41 | kind: TCSIssuer 42 | metadata: 43 | name: sgx-ca 44 | namespace: istio-system 45 | spec: 46 | secretName: istio-ca 47 | EOF 48 | ``` 49 | 50 | Update the cert-manager RBAC rules to auto approve the `CertificateRequests` for 51 | TCS issuers (`tcsissuer` and `tcsclusterissuer` in `tcs.intel.com` group): 52 | 53 | ```sh 54 | kubectl create -f deployment/cert-manager-rbac.yaml 55 | ``` 56 | 57 | Export the TCS issuer CA root certificate to `cert-manager` namespace 58 | 59 | ```sh 60 | kubectl get -n istio-system secret istio-ca -o go-template='{{index .data "tls.crt"}}' | base64 -d > ca.pem 61 | kubectl create secret generic -n cert-manager istio-root-ca --from-file=ca.pem=ca.pem 62 | ``` 63 | 64 | Deploy `istio-csr` with appropriate values: 65 | 66 | ```sh 67 | helm install -n cert-manager cert-manager-istio-csr jetstack/cert-manager-istio-csr \ 68 | --set "app.tls.rootCAFile=/var/run/secrets/istio-csr/ca.pem" \ 69 | --set "volumeMounts[0].name=root-ca" \ 70 | --set "volumeMounts[0].mountPath=/var/run/secrets/istio-csr" \ 71 | --set "volumes[0].name=root-ca" \ 72 | --set "volumes[0].secret.secretName=istio-root-ca" \ 73 | --set "app.certmanager.issuer.name=sgx-ca" \ 74 | --set "app.certmanager.issuer.kind=TCSIssuer" \ 75 | --set "app.certmanager.issuer.group=tcs.intel.com" 76 | ``` 77 | Ensure the `istio-csr` deployed is running successfully 78 | 79 | ```console 80 | $ kubectl get pod -n cert-manager -l app=cert-manager-istio-csr 81 | NAME READY STATUS RESTARTS AGE 82 | cert-manager-istio-csr-b79d7575c-ghgmk 1/1 Running 0 81s 83 | ``` 84 | 85 | Install Istio with custom configuration: 86 | 87 | ```sh 88 | curl -sSL https://raw.githubusercontent.com/cert-manager/istio-csr/main/docs/istio-config-getting-started.yaml > istio-install-config.yaml 89 | istioctl install -f istio-install-config.yaml 90 | ``` 91 | 92 | Ensure the `istio` deployed is running successfully 93 | 94 | ```sh 95 | $ kubectl get po -n istio-system 96 | NAME READY STATUS RESTARTS AGE 97 | istio-egressgateway-d5fd5f4f-6xk65 1/1 Running 0 3m 98 | istio-ingressgateway-6cd95bd9cf-crdsx 1/1 Running 0 3m 99 | istiod-f985cb778-bpnkc 1/1 Running 0 3m 100 | ``` 101 | 102 | Deploy the `bookinfo` sample application as desribed in [here](istio-custom-ca-with-csr.md) 103 | -------------------------------------------------------------------------------- /internal/self-ca/self-ca_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Intel(R) 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | package selfca_test 6 | 7 | import ( 8 | "crypto/ecdsa" 9 | "crypto/elliptic" 10 | "crypto/rand" 11 | "crypto/rsa" 12 | "crypto/x509" 13 | "crypto/x509/pkix" 14 | "testing" 15 | "time" 16 | 17 | selfca "github.com/intel/trusted-certificate-issuer/internal/self-ca" 18 | testutils "github.com/intel/trusted-certificate-issuer/test/utils" 19 | "github.com/stretchr/testify/require" 20 | ) 21 | 22 | func TestSelfCA(t *testing.T) { 23 | t.Run("missing CA key or certificate", func(t *testing.T) { 24 | ca, err := selfca.NewCA(nil, nil) 25 | require.Nil(t, ca, "missing ca key or certificate should result in nil ca") 26 | require.Error(t, err, "expected an error") 27 | 28 | key, err := rsa.GenerateKey(rand.Reader, 3072) 29 | require.NoError(t, err, "failed to create rsa key") 30 | ca, err = selfca.NewCA(key, nil) 31 | require.Nil(t, ca, "missing ca certificate should result in nil ca") 32 | require.Error(t, err, "expected an error") 33 | 34 | cert, err := testutils.NewCACertificate(key, time.Now(), time.Hour, true) 35 | require.NoError(t, err, "failed to create CA certificate") 36 | ca, err = selfca.NewCA(nil, cert) 37 | require.Nil(t, ca, "missing ca certificate should result in nil ca") 38 | require.Error(t, err, "expected an error") 39 | 40 | ca, err = selfca.NewCA(key, cert) 41 | require.NoError(t, err, "expected no error") 42 | require.NotNil(t, ca, "nil ca") 43 | }) 44 | 45 | t.Run("must fail for invalidate certificate", func(t *testing.T) { 46 | key, err := rsa.GenerateKey(rand.Reader, 3072) 47 | require.NoError(t, err, "failed to create rsa key") 48 | otherKey, err := rsa.GenerateKey(rand.Reader, 3072) 49 | require.NoError(t, err, "failed to create rsa key") 50 | 51 | cert, err := testutils.NewCACertificate(key, time.Now(), time.Hour, true) 52 | require.NoError(t, err, "failed to create CA certificate") 53 | 54 | // mismatched key and certificate 55 | ca, err := selfca.NewCA(otherKey, cert) 56 | require.Nil(t, ca, "missing ca certificate should result in nil ca") 57 | require.Error(t, err, "expected an error") 58 | 59 | cert, err = testutils.NewCACertificate(key, time.Now(), time.Hour, false) 60 | require.NoError(t, err, "failed to create CA certificate") 61 | 62 | // certificate.isCA is false 63 | ca, err = selfca.NewCA(key, cert) 64 | require.Nil(t, ca, "certificate.isCA false") 65 | require.ErrorIs(t, err, selfca.CertificateIsNotCAError) 66 | 67 | // certificate.NotBefore current time 68 | cert, err = testutils.NewCACertificate(key, time.Now().Add(2*time.Hour), time.Hour, true) 69 | require.NoError(t, err, "failed to create CA certificate") 70 | ca, err = selfca.NewCA(key, cert) 71 | require.Nil(t, ca, "certificate.isCA false") 72 | require.ErrorIs(t, err, selfca.CertificateInvalidDateError) 73 | 74 | // expired certificate (NotAfter < current time) 75 | cert, err = testutils.NewCACertificate(key, time.Now().Add(-3*time.Hour), 2*time.Hour, true) 76 | require.NoError(t, err, "failed to create CA certificate") 77 | ca, err = selfca.NewCA(key, cert) 78 | require.Nil(t, ca, "certificate.isCA false") 79 | require.ErrorIs(t, err, selfca.CertificateExpiredError) 80 | }) 81 | 82 | t.Run("sign client certificate", func(t *testing.T) { 83 | caKey, err := rsa.GenerateKey(rand.Reader, 3072) 84 | require.NoError(t, err, "failed to create rsa key") 85 | 86 | caCert, err := testutils.NewCACertificate(caKey, time.Now(), time.Hour, true) 87 | require.NoError(t, err, "failed to create CA certificate") 88 | 89 | // mismatched key and certificate 90 | ca, err := selfca.NewCA(caKey, caCert) 91 | require.NoError(t, err, "create CA") 92 | require.NotNil(t, ca, "create CA") 93 | 94 | // Key to be signed by the CA 95 | key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 96 | require.NoError(t, err, "create key") 97 | 98 | csrBytes, err := testutils.NewCertificateRequest(key, pkix.Name{CommonName: "client-service"}) 99 | require.NoError(t, err, "create CSR") 100 | 101 | _, err = ca.Sign(testutils.EncodeCSR(csrBytes), x509.KeyUsageCRLSign, nil, nil) 102 | require.NoError(t, err, "sign client certificate") 103 | }) 104 | } 105 | -------------------------------------------------------------------------------- /internal/self-ca/self-ca.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Intel(R) 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package selfca 7 | 8 | import ( 9 | "crypto" 10 | "errors" 11 | "fmt" 12 | "runtime" 13 | "time" 14 | 15 | "crypto/ecdsa" 16 | "crypto/ed25519" 17 | "crypto/rand" 18 | "crypto/rsa" 19 | "crypto/tls" 20 | "crypto/x509" 21 | "crypto/x509/pkix" 22 | 23 | "github.com/intel/trusted-certificate-issuer/internal/tlsutil" 24 | cmpki "github.com/jetstack/cert-manager/pkg/util/pki" 25 | ) 26 | 27 | var ( 28 | CertificateExpiredError = errors.New("expired") 29 | CertificateInvalidDateError = errors.New("invalid date") 30 | CertificateIsNotCAError = errors.New("certificate is not for CA") 31 | CertificateInvalidError = errors.New("invalid") 32 | ) 33 | 34 | // CA type representation for a self-signed certificate authority 35 | type CA struct { 36 | prKey crypto.Signer 37 | cert *x509.Certificate 38 | } 39 | 40 | // NewCA creates a new CA object for given CA certificate and private key. 41 | // If both of caCert and key are nil, generates a new private key and 42 | // a self-signed certificate 43 | func NewCA(key crypto.Signer, cert *x509.Certificate) (*CA, error) { 44 | if key == nil { 45 | return nil, fmt.Errorf("no key provided") 46 | } 47 | 48 | if cert == nil { 49 | return nil, fmt.Errorf("no CA certificate provided") 50 | } 51 | 52 | if err := ValidateCACertificate(cert, key.Public()); err != nil { 53 | return nil, err 54 | } 55 | 56 | ca := &CA{ 57 | prKey: key, 58 | cert: cert, 59 | } 60 | return ca, nil 61 | } 62 | 63 | // PrivateKey returns private key used 64 | func (ca *CA) PrivateKey() crypto.Signer { 65 | if ca == nil { 66 | return nil 67 | } 68 | return ca.prKey 69 | } 70 | 71 | // Certificate returns root ca certificate used 72 | func (ca *CA) Certificate() *x509.Certificate { 73 | if ca == nil { 74 | return nil 75 | } 76 | return ca.cert 77 | } 78 | 79 | // EncodedKey returns encoded private key used 80 | func (ca *CA) EncodedKey() []byte { 81 | if ca == nil { 82 | return nil 83 | } 84 | return tlsutil.EncodeKey(nil) 85 | } 86 | 87 | // EncodedCertificate returns encoded root ca certificate used 88 | func (ca *CA) EncodedCertificate() []byte { 89 | if ca == nil { 90 | return nil 91 | } 92 | return tlsutil.EncodeCert(ca.cert) 93 | } 94 | 95 | func (ca *CA) Sign(csrPEM []byte, keyUsage x509.KeyUsage, extKeyUsage []x509.ExtKeyUsage, extensions []pkix.Extension) (*x509.Certificate, error) { 96 | if ca == nil { 97 | return nil, fmt.Errorf("nil CA") 98 | } 99 | 100 | duration := time.Hour * 24 * 365 // 1 year 101 | tmpl, err := cmpki.GenerateTemplateFromCSRPEMWithUsages(csrPEM, duration, false, keyUsage, extKeyUsage) 102 | if err != nil { 103 | return nil, fmt.Errorf("failed generating certificate template: %v", err) 104 | } 105 | tmpl.Issuer = ca.cert.Issuer 106 | tmpl.Version = tls.VersionTLS12 107 | tmpl.ExtraExtensions = extensions 108 | 109 | certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, ca.cert, tmpl.PublicKey, ca.prKey) 110 | *tmpl = x509.Certificate{} 111 | if err != nil { 112 | return nil, fmt.Errorf("failed to create certificate: %v", err) 113 | } 114 | 115 | cert, err := x509.ParseCertificate(certBytes) 116 | if err != nil { 117 | return nil, fmt.Errorf("failed to parse signed certificate: %v", err) 118 | } 119 | 120 | runtime.SetFinalizer(cert, func(c *x509.Certificate) { 121 | *c = x509.Certificate{} 122 | }) 123 | 124 | return cert, nil 125 | } 126 | 127 | func ValidateCACertificate(cert *x509.Certificate, key crypto.PublicKey) error { 128 | res := false 129 | switch pub := cert.PublicKey.(type) { 130 | case *rsa.PublicKey: 131 | res = pub.Equal(key) 132 | case *ecdsa.PublicKey: 133 | res = pub.Equal(key) 134 | case ed25519.PublicKey: 135 | res = pub.Equal(key) 136 | } 137 | if !res { 138 | return fmt.Errorf("mismatched CA key and certificate") 139 | } 140 | 141 | if time.Now().Before(cert.NotBefore) { 142 | return CertificateInvalidDateError 143 | } 144 | 145 | if time.Now().UTC().After(cert.NotAfter) { 146 | return CertificateExpiredError 147 | } 148 | 149 | if !cert.IsCA { 150 | return CertificateIsNotCAError 151 | } 152 | 153 | //if cert.KeyUsage&x509.KeyUsageCertSign == 0 { 154 | // return fmt.Errorf("%s: CA certificate is not intended for certificate signing", CertificateInvalidError) 155 | //} 156 | 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /docs/istio-custom-ca-with-csr.md: -------------------------------------------------------------------------------- 1 | # Istio integration with custom CA using Kubernetes CSR 2 | 3 | Istio supports [integrating custom certificate authority(CA) using Kubernetes CSR](https://istio.io/latest/docs/tasks/security/cert-management/custom-ca-k8s/#part-2-using-custom-ca) 4 | as an experimental feature. This example shows how to provision Istio workload 5 | certificates using an Issuer provided by the Trusted Certificate Service (TCS). 6 | 7 | ## Prerequisites for running this example: 8 | 9 | - istioctl (version 1.13 or later) 10 | - Kubernetes cluster with at least one Intel SGX enabled node 11 | 12 | ## Deployment 13 | 14 | Deploy TCS and custom resource definitions (CRDs). 15 | 16 | ```sh 17 | kubectl apply -f deployment/crds/ 18 | kubectl apply -f deployment/tcs_issuer.yaml 19 | ``` 20 | 21 | Create a TCS Cluster Issuer that could sign certificates for Istio and service mesh workloads. 22 | 23 | ```sh 24 | export CA_SIGNER_NAME=sgx-signer 25 | cat << EOF | kubectl create -f - 26 | apiVersion: tcs.intel.com/v1alpha1 27 | kind: TCSClusterIssuer 28 | metadata: 29 | name: $CA_SIGNER_NAME 30 | spec: 31 | secretName: ${CA_SIGNER_NAME}-secret 32 | EOF 33 | ```` 34 | 35 | You can print the CA certificate with the command below. Note: the CA private key (tls.key) is empty since it is stored inside the SGX enclave. 36 | 37 | ```sh 38 | kubectl get secret -n tcs-issuer ${CA_SIGNER_NAME}-secret -o jsonpath='{.data.tls\.crt}' |base64 -d | sed -e 's;\(.*\); \1;g' 39 | ``` 40 | 41 | Generate Istio deployment file (`istio-custom-ca.yaml`) with custom CA configuration as below. 42 | 43 | ```sh 44 | export CA_SIGNER=tcsclusterissuer.tcs.intel.com/sgx-signer 45 | cat << EOF > istio-custom-ca.yaml 46 | apiVersion: install.istio.io/v1alpha1 47 | kind: IstioOperator 48 | spec: 49 | components: 50 | pilot: 51 | k8s: 52 | env: 53 | - name: CERT_SIGNER_DOMAIN 54 | value: tcsclusterissuer.tcs.intel.com 55 | - name: EXTERNAL_CA 56 | value: ISTIOD_RA_KUBERNETES_API 57 | - name: PILOT_CERT_PROVIDER 58 | value: k8s.io/$CA_SIGNER 59 | overlays: 60 | - kind: ClusterRole 61 | name: istiod-clusterrole-istio-system 62 | patches: 63 | - path: rules[-1] 64 | value: | 65 | apiGroups: 66 | - certificates.k8s.io 67 | resourceNames: 68 | - tcsclusterissuer.tcs.intel.com/* 69 | resources: 70 | - signers 71 | verbs: 72 | - approve 73 | meshConfig: 74 | defaultConfig: 75 | proxyMetadata: 76 | PROXY_CONFIG_XDS_AGENT: "true" 77 | ISTIO_META_CERT_SIGNER: sgx-signer 78 | caCertificates: 79 | - pem: | 80 | $(kubectl get secret -n tcs-issuer ${CA_SIGNER_NAME}-secret -o jsonpath='{.data.tls\.crt}' |base64 -d | sed -e 's;\(.*\); \1;g') 81 | certSigners: 82 | - $CA_SIGNER 83 | EOF 84 | ``` 85 | 86 | Install istio with the generated `istio-custom-ca.yaml` deployment file. 87 | 88 | 89 | ```sh 90 | istioctl install -y -f istio-custom-ca.yaml 91 | ``` 92 | 93 | ## Sample application 94 | 95 | Once the above Istio deployment is successful deploy the `bookinfo` 96 | sample application in bookinfo namespace. 97 | 98 | ```sh 99 | kubectl create ns bookinfo 100 | kubectl label ns bookinfo istio-injection=enabled 101 | kubectl apply -n bookinfo -f https://raw.githubusercontent.com/istio/istio/master/samples/bookinfo/platform/kube/bookinfo.yaml 102 | ``` 103 | 104 | Verify that the all the `bookinfo` sample application pods are in running state using the certificates signed by the Trusted Certificate Service (TCS). 105 | 106 | ```sh 107 | $ kubectl get pods -n bookinfo 108 | NAME READY STATUS RESTARTS AGE 109 | details-v1-67bc58d576-c74zl 2/2 Running 0 56s 110 | productpage-v1-7565c8c459-rqmzj 2/2 Running 0 50s 111 | ratings-v1-6485fbb4dd-hwn6n 2/2 Running 0 54s 112 | reviews-v1-545675bc9-8knfl 2/2 Running 0 52s 113 | reviews-v2-759759586-lpftf 2/2 Running 0 52s 114 | reviews-v3-bb5f95b65-phmx6 2/2 Running 0 51s 115 | ``` 116 | 117 | You can monitor the certificate signing requests (CSR) being created, approved and signed with the following command: 118 | 119 | ```sh 120 | kubectl get csr -A -w 121 | ``` 122 | -------------------------------------------------------------------------------- /deployment/crds/tcs.intel.com_tcsissuers.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | annotations: 5 | controller-gen.kubebuilder.io/version: v0.11.3 6 | creationTimestamp: null 7 | name: tcsissuers.tcs.intel.com 8 | spec: 9 | group: tcs.intel.com 10 | names: 11 | kind: TCSIssuer 12 | listKind: TCSIssuerList 13 | plural: tcsissuers 14 | singular: tcsissuer 15 | scope: Namespaced 16 | versions: 17 | - additionalPrinterColumns: 18 | - jsonPath: .metadata.creationTimestamp 19 | name: Age 20 | type: date 21 | - jsonPath: .status.conditions[?(@.type=='Ready')].status 22 | name: Ready 23 | type: string 24 | - jsonPath: .status.conditions[?(@.type=='Ready')].reason 25 | name: Reason 26 | type: string 27 | - jsonPath: .status.conditions[?(@.type=='Ready')].message 28 | name: Message 29 | type: string 30 | name: v1alpha1 31 | schema: 32 | openAPIV3Schema: 33 | description: TCSIssuer is the Schema for the issuers API 34 | properties: 35 | apiVersion: 36 | description: 'APIVersion defines the versioned schema of this representation 37 | of an object. Servers should convert recognized schemas to the latest 38 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 39 | type: string 40 | kind: 41 | description: 'Kind is a string value representing the REST resource this 42 | object represents. Servers may infer this from the endpoint the client 43 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 44 | type: string 45 | metadata: 46 | type: object 47 | spec: 48 | description: TCSIssuerSpec defines the desired state of Issuer 49 | properties: 50 | labels: 51 | additionalProperties: 52 | type: string 53 | description: Labels to set for the sub-objects (QuoteAttestation, 54 | Secret etc.,) created for this issuer. 55 | type: object 56 | secretName: 57 | description: SecretName is the name of the secret object to be created 58 | by issuer controller to hold ca certificate 59 | type: string 60 | selfSign: 61 | default: true 62 | description: SelfSignCertificate defines weather to generate a self-signed 63 | certificate for this CA issuer. When it set false, the CA is expected 64 | to get provisioned by an external key server using QuoteAttestation 65 | CRD. Default to True. 66 | type: boolean 67 | type: object 68 | status: 69 | description: TCSIssuerStatus defines the observed state of Issuer 70 | properties: 71 | conditions: 72 | description: List of status conditions to indicate the status of a 73 | CertificateRequest. Known condition types are `Ready`. 74 | items: 75 | description: IssuerCondition contains condition information for 76 | an Issuer. 77 | properties: 78 | lastTransitionTime: 79 | description: LastTransitionTime is the timestamp corresponding 80 | to the last status change of this condition. 81 | format: date-time 82 | type: string 83 | message: 84 | description: Message is a human readable description of the 85 | details of the last transition, complementing reason. 86 | type: string 87 | reason: 88 | description: Reason is a brief machine readable explanation 89 | for the condition's last transition. 90 | type: string 91 | status: 92 | description: Status of the condition, one of ('True', 'False', 93 | 'Unknown'). 94 | type: string 95 | type: 96 | description: Type of the condition, known values are ('Ready'). 97 | type: string 98 | required: 99 | - status 100 | - type 101 | type: object 102 | type: array 103 | type: object 104 | type: object 105 | served: true 106 | storage: true 107 | subresources: 108 | status: {} 109 | -------------------------------------------------------------------------------- /config/crd/tcs.intel.com_tcsissuers.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.11.3 7 | creationTimestamp: null 8 | name: tcsissuers.tcs.intel.com 9 | spec: 10 | group: tcs.intel.com 11 | names: 12 | kind: TCSIssuer 13 | listKind: TCSIssuerList 14 | plural: tcsissuers 15 | singular: tcsissuer 16 | scope: Namespaced 17 | versions: 18 | - additionalPrinterColumns: 19 | - jsonPath: .metadata.creationTimestamp 20 | name: Age 21 | type: date 22 | - jsonPath: .status.conditions[?(@.type=='Ready')].status 23 | name: Ready 24 | type: string 25 | - jsonPath: .status.conditions[?(@.type=='Ready')].reason 26 | name: Reason 27 | type: string 28 | - jsonPath: .status.conditions[?(@.type=='Ready')].message 29 | name: Message 30 | type: string 31 | name: v1alpha1 32 | schema: 33 | openAPIV3Schema: 34 | description: TCSIssuer is the Schema for the issuers API 35 | properties: 36 | apiVersion: 37 | description: 'APIVersion defines the versioned schema of this representation 38 | of an object. Servers should convert recognized schemas to the latest 39 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 40 | type: string 41 | kind: 42 | description: 'Kind is a string value representing the REST resource this 43 | object represents. Servers may infer this from the endpoint the client 44 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 45 | type: string 46 | metadata: 47 | type: object 48 | spec: 49 | description: TCSIssuerSpec defines the desired state of Issuer 50 | properties: 51 | labels: 52 | additionalProperties: 53 | type: string 54 | description: Labels to set for the sub-objects (QuoteAttestation, 55 | Secret etc.,) created for this issuer. 56 | type: object 57 | secretName: 58 | description: SecretName is the name of the secret object to be created 59 | by issuer controller to hold ca certificate 60 | type: string 61 | selfSign: 62 | default: true 63 | description: SelfSignCertificate defines weather to generate a self-signed 64 | certificate for this CA issuer. When it set false, the CA is expected 65 | to get provisioned by an external key server using QuoteAttestation 66 | CRD. Default to True. 67 | type: boolean 68 | type: object 69 | status: 70 | description: TCSIssuerStatus defines the observed state of Issuer 71 | properties: 72 | conditions: 73 | description: List of status conditions to indicate the status of a 74 | CertificateRequest. Known condition types are `Ready`. 75 | items: 76 | description: IssuerCondition contains condition information for 77 | an Issuer. 78 | properties: 79 | lastTransitionTime: 80 | description: LastTransitionTime is the timestamp corresponding 81 | to the last status change of this condition. 82 | format: date-time 83 | type: string 84 | message: 85 | description: Message is a human readable description of the 86 | details of the last transition, complementing reason. 87 | type: string 88 | reason: 89 | description: Reason is a brief machine readable explanation 90 | for the condition's last transition. 91 | type: string 92 | status: 93 | description: Status of the condition, one of ('True', 'False', 94 | 'Unknown'). 95 | type: string 96 | type: 97 | description: Type of the condition, known values are ('Ready'). 98 | type: string 99 | required: 100 | - status 101 | - type 102 | type: object 103 | type: array 104 | type: object 105 | type: object 106 | served: true 107 | storage: true 108 | subresources: 109 | status: {} 110 | -------------------------------------------------------------------------------- /deployment/crds/tcs.intel.com_tcsclusterissuers.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | annotations: 5 | controller-gen.kubebuilder.io/version: v0.11.3 6 | creationTimestamp: null 7 | name: tcsclusterissuers.tcs.intel.com 8 | spec: 9 | group: tcs.intel.com 10 | names: 11 | kind: TCSClusterIssuer 12 | listKind: TCSClusterIssuerList 13 | plural: tcsclusterissuers 14 | singular: tcsclusterissuer 15 | scope: Cluster 16 | versions: 17 | - additionalPrinterColumns: 18 | - jsonPath: .metadata.creationTimestamp 19 | name: Age 20 | type: date 21 | - jsonPath: .status.conditions[?(@.type=='Ready')].status 22 | name: Ready 23 | type: string 24 | - jsonPath: .status.conditions[?(@.type=='Ready')].reason 25 | name: Reason 26 | type: string 27 | - jsonPath: .status.conditions[?(@.type=='Ready')].message 28 | name: Message 29 | type: string 30 | name: v1alpha1 31 | schema: 32 | openAPIV3Schema: 33 | description: TCSClusterIssuer is the Schema for the clusterissuers API 34 | properties: 35 | apiVersion: 36 | description: 'APIVersion defines the versioned schema of this representation 37 | of an object. Servers should convert recognized schemas to the latest 38 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 39 | type: string 40 | kind: 41 | description: 'Kind is a string value representing the REST resource this 42 | object represents. Servers may infer this from the endpoint the client 43 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 44 | type: string 45 | metadata: 46 | type: object 47 | spec: 48 | description: TCSIssuerSpec defines the desired state of Issuer 49 | properties: 50 | labels: 51 | additionalProperties: 52 | type: string 53 | description: Labels to set for the sub-objects (QuoteAttestation, 54 | Secret etc.,) created for this issuer. 55 | type: object 56 | secretName: 57 | description: SecretName is the name of the secret object to be created 58 | by issuer controller to hold ca certificate 59 | type: string 60 | selfSign: 61 | default: true 62 | description: SelfSignCertificate defines weather to generate a self-signed 63 | certificate for this CA issuer. When it set false, the CA is expected 64 | to get provisioned by an external key server using QuoteAttestation 65 | CRD. Default to True. 66 | type: boolean 67 | type: object 68 | status: 69 | description: TCSIssuerStatus defines the observed state of Issuer 70 | properties: 71 | conditions: 72 | description: List of status conditions to indicate the status of a 73 | CertificateRequest. Known condition types are `Ready`. 74 | items: 75 | description: IssuerCondition contains condition information for 76 | an Issuer. 77 | properties: 78 | lastTransitionTime: 79 | description: LastTransitionTime is the timestamp corresponding 80 | to the last status change of this condition. 81 | format: date-time 82 | type: string 83 | message: 84 | description: Message is a human readable description of the 85 | details of the last transition, complementing reason. 86 | type: string 87 | reason: 88 | description: Reason is a brief machine readable explanation 89 | for the condition's last transition. 90 | type: string 91 | status: 92 | description: Status of the condition, one of ('True', 'False', 93 | 'Unknown'). 94 | type: string 95 | type: 96 | description: Type of the condition, known values are ('Ready'). 97 | type: string 98 | required: 99 | - status 100 | - type 101 | type: object 102 | type: array 103 | type: object 104 | type: object 105 | served: true 106 | storage: true 107 | subresources: 108 | status: {} 109 | -------------------------------------------------------------------------------- /config/crd/tcs.intel.com_tcsclusterissuers.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.11.3 7 | creationTimestamp: null 8 | name: tcsclusterissuers.tcs.intel.com 9 | spec: 10 | group: tcs.intel.com 11 | names: 12 | kind: TCSClusterIssuer 13 | listKind: TCSClusterIssuerList 14 | plural: tcsclusterissuers 15 | singular: tcsclusterissuer 16 | scope: Cluster 17 | versions: 18 | - additionalPrinterColumns: 19 | - jsonPath: .metadata.creationTimestamp 20 | name: Age 21 | type: date 22 | - jsonPath: .status.conditions[?(@.type=='Ready')].status 23 | name: Ready 24 | type: string 25 | - jsonPath: .status.conditions[?(@.type=='Ready')].reason 26 | name: Reason 27 | type: string 28 | - jsonPath: .status.conditions[?(@.type=='Ready')].message 29 | name: Message 30 | type: string 31 | name: v1alpha1 32 | schema: 33 | openAPIV3Schema: 34 | description: TCSClusterIssuer is the Schema for the clusterissuers API 35 | properties: 36 | apiVersion: 37 | description: 'APIVersion defines the versioned schema of this representation 38 | of an object. Servers should convert recognized schemas to the latest 39 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 40 | type: string 41 | kind: 42 | description: 'Kind is a string value representing the REST resource this 43 | object represents. Servers may infer this from the endpoint the client 44 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 45 | type: string 46 | metadata: 47 | type: object 48 | spec: 49 | description: TCSIssuerSpec defines the desired state of Issuer 50 | properties: 51 | labels: 52 | additionalProperties: 53 | type: string 54 | description: Labels to set for the sub-objects (QuoteAttestation, 55 | Secret etc.,) created for this issuer. 56 | type: object 57 | secretName: 58 | description: SecretName is the name of the secret object to be created 59 | by issuer controller to hold ca certificate 60 | type: string 61 | selfSign: 62 | default: true 63 | description: SelfSignCertificate defines weather to generate a self-signed 64 | certificate for this CA issuer. When it set false, the CA is expected 65 | to get provisioned by an external key server using QuoteAttestation 66 | CRD. Default to True. 67 | type: boolean 68 | type: object 69 | status: 70 | description: TCSIssuerStatus defines the observed state of Issuer 71 | properties: 72 | conditions: 73 | description: List of status conditions to indicate the status of a 74 | CertificateRequest. Known condition types are `Ready`. 75 | items: 76 | description: IssuerCondition contains condition information for 77 | an Issuer. 78 | properties: 79 | lastTransitionTime: 80 | description: LastTransitionTime is the timestamp corresponding 81 | to the last status change of this condition. 82 | format: date-time 83 | type: string 84 | message: 85 | description: Message is a human readable description of the 86 | details of the last transition, complementing reason. 87 | type: string 88 | reason: 89 | description: Reason is a brief machine readable explanation 90 | for the condition's last transition. 91 | type: string 92 | status: 93 | description: Status of the condition, one of ('True', 'False', 94 | 'Unknown'). 95 | type: string 96 | type: 97 | description: Type of the condition, known values are ('Ready'). 98 | type: string 99 | required: 100 | - status 101 | - type 102 | type: object 103 | type: array 104 | type: object 105 | type: object 106 | served: true 107 | storage: true 108 | subresources: 109 | status: {} 110 | -------------------------------------------------------------------------------- /api/v1alpha2/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright 2021 Intel(R). 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Code generated by controller-gen. DO NOT EDIT. 21 | 22 | package v1alpha2 23 | 24 | import ( 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *QuoteAttestation) DeepCopyInto(out *QuoteAttestation) { 30 | *out = *in 31 | out.TypeMeta = in.TypeMeta 32 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 33 | in.Spec.DeepCopyInto(&out.Spec) 34 | in.Status.DeepCopyInto(&out.Status) 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QuoteAttestation. 38 | func (in *QuoteAttestation) DeepCopy() *QuoteAttestation { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(QuoteAttestation) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 48 | func (in *QuoteAttestation) DeepCopyObject() runtime.Object { 49 | if c := in.DeepCopy(); c != nil { 50 | return c 51 | } 52 | return nil 53 | } 54 | 55 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 56 | func (in *QuoteAttestationCondition) DeepCopyInto(out *QuoteAttestationCondition) { 57 | *out = *in 58 | in.LastUpdateTime.DeepCopyInto(&out.LastUpdateTime) 59 | } 60 | 61 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QuoteAttestationCondition. 62 | func (in *QuoteAttestationCondition) DeepCopy() *QuoteAttestationCondition { 63 | if in == nil { 64 | return nil 65 | } 66 | out := new(QuoteAttestationCondition) 67 | in.DeepCopyInto(out) 68 | return out 69 | } 70 | 71 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 72 | func (in *QuoteAttestationList) DeepCopyInto(out *QuoteAttestationList) { 73 | *out = *in 74 | out.TypeMeta = in.TypeMeta 75 | in.ListMeta.DeepCopyInto(&out.ListMeta) 76 | if in.Items != nil { 77 | in, out := &in.Items, &out.Items 78 | *out = make([]QuoteAttestation, len(*in)) 79 | for i := range *in { 80 | (*in)[i].DeepCopyInto(&(*out)[i]) 81 | } 82 | } 83 | } 84 | 85 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QuoteAttestationList. 86 | func (in *QuoteAttestationList) DeepCopy() *QuoteAttestationList { 87 | if in == nil { 88 | return nil 89 | } 90 | out := new(QuoteAttestationList) 91 | in.DeepCopyInto(out) 92 | return out 93 | } 94 | 95 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 96 | func (in *QuoteAttestationList) DeepCopyObject() runtime.Object { 97 | if c := in.DeepCopy(); c != nil { 98 | return c 99 | } 100 | return nil 101 | } 102 | 103 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 104 | func (in *QuoteAttestationSpec) DeepCopyInto(out *QuoteAttestationSpec) { 105 | *out = *in 106 | if in.Quote != nil { 107 | in, out := &in.Quote, &out.Quote 108 | *out = make([]byte, len(*in)) 109 | copy(*out, *in) 110 | } 111 | if in.Nonce != nil { 112 | in, out := &in.Nonce, &out.Nonce 113 | *out = make([]byte, len(*in)) 114 | copy(*out, *in) 115 | } 116 | if in.PublicKey != nil { 117 | in, out := &in.PublicKey, &out.PublicKey 118 | *out = make([]byte, len(*in)) 119 | copy(*out, *in) 120 | } 121 | } 122 | 123 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QuoteAttestationSpec. 124 | func (in *QuoteAttestationSpec) DeepCopy() *QuoteAttestationSpec { 125 | if in == nil { 126 | return nil 127 | } 128 | out := new(QuoteAttestationSpec) 129 | in.DeepCopyInto(out) 130 | return out 131 | } 132 | 133 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 134 | func (in *QuoteAttestationStatus) DeepCopyInto(out *QuoteAttestationStatus) { 135 | *out = *in 136 | if in.Conditions != nil { 137 | in, out := &in.Conditions, &out.Conditions 138 | *out = make([]QuoteAttestationCondition, len(*in)) 139 | for i := range *in { 140 | (*in)[i].DeepCopyInto(&(*out)[i]) 141 | } 142 | } 143 | } 144 | 145 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QuoteAttestationStatus. 146 | func (in *QuoteAttestationStatus) DeepCopy() *QuoteAttestationStatus { 147 | if in == nil { 148 | return nil 149 | } 150 | out := new(QuoteAttestationStatus) 151 | in.DeepCopyInto(out) 152 | return out 153 | } 154 | -------------------------------------------------------------------------------- /api/v1alpha1/tcsissuer_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Intel(R) Corporation. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | v1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // TCSIssuerSpec defines the desired state of Issuer 25 | type TCSIssuerSpec struct { 26 | // Labels to set for the sub-objects (QuoteAttestation, Secret etc.,) 27 | // created for this issuer. 28 | // +optional 29 | Labels map[string]string `json:"labels,omitempty"` 30 | 31 | // SecretName is the name of the secret object to be 32 | // created by issuer controller to hold ca certificate 33 | SecretName string `json:"secretName,omitempty"` 34 | // SelfSignCertificate defines weather to generate a self-signed certificate 35 | // for this CA issuer. When it set false, the CA is expected to get provisioned 36 | // by an external key server using QuoteAttestation CRD. 37 | // Default to True. 38 | // +kubebuilder:default=true 39 | SelfSignCertificate *bool `json:"selfSign,omitempty"` 40 | } 41 | 42 | // TCSIssuerStatus defines the observed state of Issuer 43 | type TCSIssuerStatus struct { 44 | // List of status conditions to indicate the status of a CertificateRequest. 45 | // Known condition types are `Ready`. 46 | // +optional 47 | Conditions []TCSIssuerCondition `json:"conditions,omitempty"` 48 | } 49 | 50 | // +kubebuilder:object:root=true 51 | // +kubebuilder:subresource:status 52 | // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=`.metadata.creationTimestamp` 53 | // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=`.status.conditions[?(@.type=='Ready')].status` 54 | // +kubebuilder:printcolumn:name="Reason",type="string",JSONPath=`.status.conditions[?(@.type=='Ready')].reason` 55 | // +kubebuilder:printcolumn:name="Message",type="string",JSONPath=`.status.conditions[?(@.type=='Ready')].message` 56 | // TCSIssuer is the Schema for the issuers API 57 | type TCSIssuer struct { 58 | metav1.TypeMeta `json:",inline"` 59 | metav1.ObjectMeta `json:"metadata,omitempty"` 60 | 61 | Spec TCSIssuerSpec `json:"spec,omitempty"` 62 | Status TCSIssuerStatus `json:"status,omitempty"` 63 | } 64 | 65 | // +kubebuilder:object:root=true 66 | // TCSIssuerList contains a list of TCSIssuer 67 | type TCSIssuerList struct { 68 | metav1.TypeMeta `json:",inline"` 69 | metav1.ListMeta `json:"metadata,omitempty"` 70 | Items []TCSIssuer `json:"items"` 71 | } 72 | 73 | // IssuerCondition contains condition information for an Issuer. 74 | type TCSIssuerCondition struct { 75 | // Type of the condition, known values are ('Ready'). 76 | Type IssuerConditionType `json:"type"` 77 | 78 | // Status of the condition, one of ('True', 'False', 'Unknown'). 79 | Status v1.ConditionStatus `json:"status"` 80 | 81 | // LastTransitionTime is the timestamp corresponding to the last status 82 | // change of this condition. 83 | // +optional 84 | LastTransitionTime *metav1.Time `json:"lastTransitionTime,omitempty"` 85 | 86 | // Reason is a brief machine readable explanation for the condition's last 87 | // transition. 88 | // +optional 89 | Reason string `json:"reason,omitempty"` 90 | 91 | // Message is a human readable description of the details of the last 92 | // transition, complementing reason. 93 | // +optional 94 | Message string `json:"message,omitempty"` 95 | } 96 | 97 | // IssuerConditionType represents an Issuer condition value. 98 | type IssuerConditionType string 99 | 100 | const ( 101 | // IssuerConditionReady represents the fact that a given Issuer condition 102 | // is in ready state and able to issue certificates. 103 | // If the `status` of this condition is `False`, CertificateRequest controllers 104 | // should prevent attempts to sign certificates. 105 | IssuerConditionReady IssuerConditionType = "Ready" 106 | ) 107 | 108 | func init() { 109 | SchemeBuilder.Register(&TCSIssuer{}, &TCSIssuerList{}) 110 | } 111 | 112 | func (status *TCSIssuerStatus) GetCondition(ct IssuerConditionType) *TCSIssuerCondition { 113 | for _, c := range status.Conditions { 114 | if c.Type == ct { 115 | return &c 116 | } 117 | } 118 | return nil 119 | } 120 | 121 | func (status *TCSIssuerStatus) SetCondition(ct IssuerConditionType, condStatus v1.ConditionStatus, reason, message string) { 122 | cond := status.GetCondition(ct) 123 | if cond == nil { 124 | cond = &TCSIssuerCondition{ 125 | Type: ct, 126 | Status: condStatus, 127 | } 128 | status.Conditions = append(status.Conditions, *cond) 129 | } 130 | cond.Status = condStatus 131 | cond.Message = message 132 | cond.Reason = reason 133 | if cond.Status == condStatus { 134 | cond.Status = condStatus 135 | now := metav1.Now() 136 | cond.LastTransitionTime = &now 137 | } 138 | for i, c := range status.Conditions { 139 | if c.Type == ct { 140 | status.Conditions[i] = *cond 141 | return 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REGISTRY ?= docker.io 2 | IMG_NAME ?= intel/trusted-certificate-issuer 3 | IMG_TAG ?= latest 4 | # Image URL to use all building/pushing image targets 5 | IMG ?= $(REGISTRY)/$(IMG_NAME):$(IMG_TAG) 6 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 7 | CRD_OPTIONS ?= crd 8 | 9 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 10 | ifeq (,$(shell go env GOBIN)) 11 | GOBIN=$(shell go env GOPATH)/bin 12 | else 13 | GOBIN=$(shell go env GOBIN) 14 | endif 15 | 16 | # Setting SHELL to bash allows bash commands to be executed by recipes. 17 | # This is a requirement for 'setup-envtest.sh' in the test target. 18 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 19 | SHELL = /usr/bin/env bash -o pipefail 20 | .SHELLFLAGS = -ec 21 | 22 | all: build 23 | 24 | ##@ General 25 | 26 | # The help target prints out all targets with their descriptions organized 27 | # beneath their categories. The categories are represented by '##@' and the 28 | # target descriptions by '##'. The awk commands is responsible for reading the 29 | # entire set of makefiles included in this invocation, looking for lines of the 30 | # file as xyz: ## something, and then pretty-format the target and help. Then, 31 | # if there's a line with ##@ something, that gets pretty-printed as a category. 32 | # More info on the usage of ANSI control characters for terminal formatting: 33 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 34 | # More info on the awk command: 35 | # http://linuxcommand.org/lc3_adv_awk.php 36 | 37 | help: ## Display this help. 38 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 39 | 40 | ##@ Development 41 | 42 | vendor: 43 | go mod tidy 44 | go mod vendor 45 | 46 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 47 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=role paths="./controllers/..." paths="./api/..." 48 | 49 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 50 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./api/..." 51 | 52 | fmt: ## Run go fmt against code. 53 | @go fmt ./... 54 | 55 | vet: ## Run go vet against code. 56 | @go vet ./... 57 | 58 | ENVTEST_ASSETS_DIR=$(shell pwd)/testbin 59 | test: vendor manifests fmt ## Run tests. 60 | mkdir -p ${ENVTEST_ASSETS_DIR} 61 | test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.7.2/hack/setup-envtest.sh 62 | source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); ACK_GINKGO_DEPRECATIONS=1.16.4 go test ./... -coverprofile cover.out 63 | 64 | ##@ Build 65 | 66 | build: generate fmt vet ## Build manager binary. 67 | go build --buildmode=pie -o bin/tcs-issuer main.go 68 | 69 | run: manifests generate fmt vet ## Run a controller from your host. 70 | go run ./main.go 71 | 72 | # Latest CTK commit id as of 31.03.2022 which includes 73 | # mitigation for key export vulnerability. 74 | # 75 | # Keep update this to include the latest CTK code changes. 76 | CTK_TAG ?= 91ee4968b7b97996f8c466a3ebbdce41168118e3 77 | 78 | # additional arguments to pass to 'docker build' 79 | BUILD_ARGS ?= 80 | BUILD_ARGS := $(BUILD_ARGS) --build-arg CTK_TAG=${CTK_TAG} 81 | # Adjust this argument and accodingly the 'enclave-config/sign-enclave.sh' 82 | # script in CI build system to reflect with the right private key 83 | # and/or signing with external tool. 84 | DOCKER_BUILD_DEPS ?= enclave-config/privatekey.pem 85 | docker-build: ${DOCKER_BUILD_DEPS} vendor ## Build docker image with the manager. 86 | docker build ${BUILD_ARGS} -t ${IMG} . 87 | 88 | enclave-config/privatekey.pem: 89 | openssl genrsa -3 -out enclave-config/privatekey.pem 3072 90 | 91 | docker-push: ## Push docker image with the manager. 92 | docker push ${IMG} 93 | 94 | ##@ Deployment 95 | 96 | install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 97 | $(KUSTOMIZE) build config/crd | kubectl apply -f - 98 | 99 | uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. 100 | $(KUSTOMIZE) build config/crd | kubectl delete -f - 101 | 102 | deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 103 | cd config/manager && $(KUSTOMIZE) edit set image tcs-issuer=${IMG} 104 | $(KUSTOMIZE) build config/default | kubectl apply -f - 105 | 106 | deploy-manifests: manifests kustomize 107 | cd config/manager && $(KUSTOMIZE) edit set image tcs-issuer=${IMG} 108 | mkdir -p deployment && $(KUSTOMIZE) build config/default -o deployment/tcs_issuer.yaml 109 | mkdir -p deployment/crds && $(KUSTOMIZE) build -o deployment/crds config/crd 110 | ## Rename CRDs identical to names generated by the controller_gen; remove prefixed type information 111 | @cd deployment/crds; for f in $$(ls ./apiextensions*.yaml); do newname=$$(echo $$f|sed -e 's|apiextensions.k8s.io_v1_customresourcedefinition_\(.*\).\(tcs.intel.com*\)|\2_\1|g'); mv $$f $$newname; done 112 | 113 | undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. 114 | $(KUSTOMIZE) build config/default | kubectl delete -f - 115 | 116 | VERSION ?= 117 | release-branch: 118 | ifeq ("$(VERSION)", "") 119 | $(error "Set release version using VERSION make variable. Example: `make release VERSION=0.1.0` ") 120 | endif 121 | ./hack/prepare-release-branch.sh --version $(VERSION) 122 | 123 | CONTROLLER_GEN = $(shell pwd)/bin/controller-gen 124 | controller-gen: ## Download controller-gen locally if necessary. 125 | $(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.11.3) 126 | 127 | KUSTOMIZE = $(shell pwd)/bin/kustomize 128 | kustomize: ## Download kustomize locally if necessary. 129 | $(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5@v5.0.1) 130 | 131 | # go-get-tool will 'go get' any package $2 and install it to $1. 132 | PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) 133 | define go-get-tool 134 | @[ -f $(1) ] || { \ 135 | set -e ;\ 136 | TMP_DIR=$$(mktemp -d) ;\ 137 | cd $$TMP_DIR ;\ 138 | go mod init tmp ;\ 139 | echo "Downloading $(2)" ;\ 140 | GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\ 141 | rm -rf $$TMP_DIR ;\ 142 | } 143 | endef 144 | 145 | helm: 146 | cp -rf deployment/crds charts 147 | helm package charts 148 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Intel(R). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "os" 22 | 23 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 24 | // to ensure that exec-entrypoint and run can make use of them. 25 | cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | _ "k8s.io/apimachinery/pkg/types" 28 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 29 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/healthz" 32 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 33 | 34 | tcsapi "github.com/intel/trusted-certificate-issuer/api/v1alpha1" 35 | "github.com/intel/trusted-certificate-issuer/api/v1alpha2" 36 | "github.com/intel/trusted-certificate-issuer/controllers" 37 | "github.com/intel/trusted-certificate-issuer/internal/config" 38 | "github.com/intel/trusted-certificate-issuer/internal/sgx" 39 | //+kubebuilder:scaffold:imports 40 | ) 41 | 42 | const ( 43 | DefaultQuoteVersion = "ECDSA Quote 3" 44 | instanceName = "sgx.quote.attestation.deliver" 45 | quoteAttestationCtrlr = "quote-attestation-controller" 46 | ) 47 | 48 | var scheme = runtime.NewScheme() 49 | 50 | func init() { 51 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 52 | utilruntime.Must(tcsapi.AddToScheme(scheme)) 53 | utilruntime.Must(v1alpha2.AddToScheme(scheme)) 54 | utilruntime.Must(cmapi.AddToScheme(scheme)) 55 | //+kubebuilder:scaffold:scheme 56 | } 57 | 58 | func main() { 59 | cfg := config.Config{} 60 | 61 | flag.StringVar(&cfg.MetricsAddress, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 62 | flag.StringVar(&cfg.HealthProbeAddress, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 63 | flag.BoolVar(&cfg.LeaderElection, "leader-elect", false, 64 | "Enable leader election for controller manager. "+ 65 | "Enabling this will ensure there is only one active controller manager.") 66 | flag.BoolVar(&cfg.CertManagerIssuer, "cert-manager-issuer", true, "Run it as issuer for cert-manager.") 67 | flag.StringVar(&cfg.HSMConfigPath, "hsm-config-file", "/etc/hsm/config.json", "File path that holds HSM configuration in JSON format(so/user pin, token label etc).") 68 | flag.StringVar(&cfg.HSMConfig.TokenLabel, "token-label", "SgxOperator", "[Deprecated]: PKCS11 label to use for the operator token.") 69 | flag.StringVar(&cfg.HSMConfig.UserPin, "user-pin", "", "[Deprecated]: PKCS11 token user pin.") 70 | flag.StringVar(&cfg.HSMConfig.SoPin, "so-pin", "", "[Deprecated]: PKCS11 token so/admin pin.") 71 | flag.BoolVar(&cfg.CSRFullCertChain, "csr-full-cert-chain", false, "Return full certificate chain in Kubernetes CSR certificate.") 72 | flag.BoolVar(&cfg.RandomNonce, "use-random-nonce", true, "Use random nonce for SGX quote generation. Needed for KMRA version >= v2.2.") 73 | flag.StringVar(&cfg.KeyWrapMechanism, "key-wrap-mechanism", config.KeyWrapAesKeyWrapPad, "CA private key wrapping mechanism to use. One of: '"+config.KeyWrapAesGCM+"' or '"+config.KeyWrapAesKeyWrapPad+"'.") 74 | 75 | opts := zap.Options{} 76 | opts.BindFlags(flag.CommandLine) 77 | flag.Parse() 78 | 79 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 80 | 81 | setupLog := ctrl.Log.WithName("setup") 82 | 83 | if err := cfg.Validate(); err != nil { 84 | setupLog.Error(err, "Invalid operator configuration") 85 | os.Exit(1) 86 | } 87 | 88 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 89 | Scheme: scheme, 90 | MetricsBindAddress: cfg.MetricsAddress, 91 | Port: 9443, 92 | HealthProbeBindAddress: cfg.HealthProbeAddress, 93 | LeaderElection: cfg.LeaderElection, 94 | LeaderElectionID: "bb9c3a43.sgx.intel.com", 95 | }) 96 | if err != nil { 97 | setupLog.Error(err, "unable to start manager") 98 | os.Exit(1) 99 | } 100 | 101 | sgxctx, err := sgx.NewContext(cfg, mgr.GetClient()) 102 | if err != nil { 103 | setupLog.Error(err, "SGX initialization") 104 | os.Exit(1) 105 | } 106 | setupLog.V(2).Info("SGX initialization SUCCESS") 107 | defer sgxctx.Destroy() 108 | 109 | if err = (&controllers.IssuerReconciler{ 110 | Client: mgr.GetClient(), 111 | Log: ctrl.Log.WithName("controllers").WithName("TCSIssuer"), 112 | Kind: "TCSIssuer", 113 | Scheme: mgr.GetScheme(), 114 | KeyProvider: sgxctx, 115 | }).SetupWithManager(mgr); err != nil { 116 | setupLog.Error(err, "unable to create controller", "controller", "TCSIssuer") 117 | os.Exit(1) 118 | } 119 | if err = (&controllers.IssuerReconciler{ 120 | Client: mgr.GetClient(), 121 | Log: ctrl.Log.WithName("controllers").WithName("TCSClusterIssuer"), 122 | Kind: "TCSClusterIssuer", 123 | Scheme: mgr.GetScheme(), 124 | KeyProvider: sgxctx, 125 | }).SetupWithManager(mgr); err != nil { 126 | setupLog.Error(err, "unable to create controller", "controller", "TCSClusterIssuer") 127 | os.Exit(1) 128 | } 129 | 130 | if err = controllers.NewCSRReconciler(mgr.GetClient(), mgr.GetScheme(), sgxctx, cfg.CSRFullCertChain).SetupWithManager(mgr); err != nil { 131 | setupLog.Error(err, "unable to create controller", "controller", "CSR") 132 | os.Exit(1) 133 | } 134 | 135 | if cfg.CertManagerIssuer { 136 | if err = (&controllers.CertificateRequestReconciler{ 137 | Client: mgr.GetClient(), 138 | Log: ctrl.Log.WithName("controllers").WithName("CertificateRequest"), 139 | Scheme: mgr.GetScheme(), 140 | KeyProvider: sgxctx, 141 | }).SetupWithManager(mgr); err != nil { 142 | setupLog.Error(err, "unable to create controller", "controller", "CertificateRequest") 143 | os.Exit(1) 144 | } 145 | } 146 | 147 | //+kubebuilder:scaffold:builder 148 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 149 | setupLog.Error(err, "unable to set up health check") 150 | os.Exit(1) 151 | } 152 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 153 | setupLog.Error(err, "unable to set up ready check") 154 | os.Exit(1) 155 | } 156 | 157 | setupLog.Info("starting manager") 158 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 159 | setupLog.Error(err, "problem running manager") 160 | os.Exit(1) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /internal/k8sutil/k8sutil.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Intel Coporation. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package k8sutil 7 | 8 | import ( 9 | "context" 10 | "crypto/x509" 11 | "encoding/base64" 12 | "fmt" 13 | "io/ioutil" 14 | "os" 15 | "strings" 16 | 17 | tcsapi "github.com/intel/trusted-certificate-issuer/api/v1alpha2" 18 | "github.com/intel/trusted-certificate-issuer/internal/keyprovider" 19 | "github.com/intel/trusted-certificate-issuer/internal/tlsutil" 20 | v1 "k8s.io/api/core/v1" 21 | "k8s.io/apimachinery/pkg/api/errors" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | "k8s.io/apimachinery/pkg/types" 24 | "k8s.io/klog/v2" 25 | "sigs.k8s.io/controller-runtime/pkg/client" 26 | ) 27 | 28 | const ( 29 | namespaceEnvVar = "WATCH_NAMESPACE" 30 | namespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" 31 | TCSFinalizer = "tcs.intel.com/issuer-protection" 32 | ) 33 | 34 | // GetNamespace returns the namespace of the operator pod 35 | func GetNamespace() string { 36 | ns := os.Getenv(namespaceEnvVar) 37 | if ns == "" { 38 | // If environment variable not set, give it a try to fetch it from 39 | // mounted filesystem by Kubernetes 40 | data, err := ioutil.ReadFile(namespaceFile) 41 | if err != nil { 42 | klog.Infof("Could not read namespace from %q: %v", namespaceFile, err) 43 | } else { 44 | ns = string(data) 45 | } 46 | } 47 | 48 | if ns == "" { 49 | ns = metav1.NamespaceDefault 50 | } 51 | 52 | return ns 53 | } 54 | 55 | func CreateCASecret(ctx context.Context, c client.Client, cert *x509.Certificate, name, ns string, owner metav1.OwnerReference, labels map[string]string) error { 56 | if ns == "" { 57 | ns = GetNamespace() 58 | } 59 | secret := &v1.Secret{ 60 | ObjectMeta: metav1.ObjectMeta{ 61 | Name: name, 62 | Namespace: ns, 63 | OwnerReferences: []metav1.OwnerReference{owner}, 64 | Labels: labels, 65 | Finalizers: []string{TCSFinalizer}, 66 | }, 67 | Type: v1.SecretTypeTLS, 68 | Data: map[string][]byte{ 69 | v1.TLSPrivateKeyKey: []byte(""), 70 | v1.TLSCertKey: tlsutil.EncodeCert(cert), 71 | }, 72 | } 73 | err := c.Create(ctx, secret) 74 | if err != nil && errors.IsAlreadyExists(err) { 75 | return c.Update(ctx, secret) 76 | } 77 | return err 78 | } 79 | 80 | func DeleteCASecret(ctx context.Context, c client.Client, name, ns string) error { 81 | if ns == "" { 82 | ns = GetNamespace() 83 | } 84 | secret := &v1.Secret{ 85 | ObjectMeta: metav1.ObjectMeta{ 86 | Name: name, 87 | Namespace: ns, 88 | }, 89 | } 90 | 91 | if err := UnsetFinalizer(ctx, c, secret, func() client.Object { 92 | return secret.DeepCopy() 93 | }); err != nil { 94 | return err 95 | } 96 | 97 | return client.IgnoreNotFound(c.Delete(ctx, secret)) 98 | } 99 | 100 | func QuoteAttestationDeliver( 101 | ctx context.Context, 102 | c client.Client, 103 | req types.NamespacedName, 104 | requestType tcsapi.QuoteAttestationRequestType, 105 | signerName string, 106 | quoteInfo *keyprovider.QuoteInfo, 107 | tokenLabel string, 108 | ownerRef *metav1.OwnerReference, 109 | labels map[string]string) error { 110 | 111 | encPubKey, err := tlsutil.EncodePublicKey(quoteInfo.PublicKey) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | encQuote := base64.StdEncoding.EncodeToString(quoteInfo.Quote) 117 | encNonce := base64.StdEncoding.EncodeToString(quoteInfo.Nonce) 118 | 119 | if req.Namespace == "" { 120 | req.Namespace = GetNamespace() 121 | } 122 | 123 | sgxAttestation := &tcsapi.QuoteAttestation{ 124 | ObjectMeta: metav1.ObjectMeta{ 125 | Name: req.Name, 126 | Namespace: req.Namespace, 127 | OwnerReferences: []metav1.OwnerReference{ 128 | *ownerRef, 129 | }, 130 | Labels: labels, 131 | Finalizers: []string{TCSFinalizer}, 132 | }, 133 | Spec: tcsapi.QuoteAttestationSpec{ 134 | Type: requestType, 135 | Quote: []byte(encQuote), 136 | Nonce: []byte(encNonce), 137 | QuoteVersion: tcsapi.ECDSAQuoteVersion3, 138 | SignerName: signerName, 139 | ServiceID: tokenLabel, 140 | PublicKey: encPubKey, 141 | // Using the QuoteAttestation CR name for storing encrypted CA secret 142 | SecretName: req.Name, 143 | }, 144 | } 145 | 146 | //Create a CR instance for QuoteAttestation 147 | //If not found object, return a new one 148 | err = c.Create(ctx, sgxAttestation) 149 | if err != nil { 150 | if errors.IsAlreadyExists(err) { 151 | if err = QuoteAttestationDelete(ctx, c, req); err != nil { 152 | return fmt.Errorf("failed to delete existing QuoteAttestation CR with name '%s'. Clear this before redeploy the operator: %v", req.Name, err) 153 | } 154 | 155 | err = c.Create(ctx, sgxAttestation) 156 | } 157 | } 158 | return err 159 | } 160 | 161 | func QuoteAttestationDelete(ctx context.Context, c client.Client, req types.NamespacedName) error { 162 | if req.Namespace == "" { 163 | req.Namespace = GetNamespace() 164 | } 165 | sgxAttestation := &tcsapi.QuoteAttestation{ 166 | ObjectMeta: metav1.ObjectMeta{ 167 | Name: req.Name, 168 | Namespace: req.Namespace, 169 | }, 170 | } 171 | 172 | if err := UnsetFinalizer(ctx, c, sgxAttestation, func() client.Object { 173 | return sgxAttestation.DeepCopy() 174 | }); err != nil { 175 | return fmt.Errorf("failed unset finalizer for '%v': %v", req, err) 176 | } 177 | 178 | return client.IgnoreNotFound(c.Delete(ctx, sgxAttestation)) 179 | } 180 | 181 | // Converts signer name to valid Kubernetes object name and nanespace 182 | // 183 | // Ex:- intel.com/tcs -> tcs.intel.com, "" 184 | // 185 | // / tcsissuer.tcs.intel.com/sandbox.sgx-ca -> sgx-ca.tcs.intel.com, sandbox 186 | // 187 | // tcsclusterissuer.tcs.intel.com/sgx-ca1 -> sgx-ca1.tcsclusterissuer.intel.tcs.com, "" 188 | func SignerNameToResourceNameAndNamespace(signerName string) (string, string) { 189 | slices := strings.SplitN(signerName, "/", 2) 190 | if len(slices) == 2 { 191 | nameParts := strings.SplitN(slices[1], ".", 2) 192 | if len(nameParts) == 2 { 193 | return nameParts[1] + "." + slices[0], nameParts[0] 194 | } 195 | return slices[1] + "." + slices[0], "" 196 | } 197 | 198 | return slices[0], "" 199 | } 200 | 201 | func UnsetFinalizer(ctx context.Context, c client.Client, obj client.Object, copier func() client.Object) error { 202 | key := client.ObjectKeyFromObject(obj) 203 | if err := client.IgnoreNotFound(c.Get(ctx, key, obj)); err != nil { 204 | return err 205 | } 206 | 207 | list := obj.GetFinalizers() 208 | found := false 209 | for i, finalizer := range list { 210 | if finalizer == TCSFinalizer { 211 | found = true 212 | list = append(list[:i], list[i+1:]...) 213 | break 214 | } 215 | } 216 | 217 | if found { 218 | patch := client.MergeFrom(copier()) 219 | obj.SetFinalizers(list) 220 | if err := client.IgnoreNotFound(c.Patch(ctx, obj, patch)); err != nil { 221 | return fmt.Errorf("failed to patch object (%v) with update finalizer : %v", key, err) 222 | } 223 | } 224 | return nil 225 | } 226 | -------------------------------------------------------------------------------- /api/v1alpha1/quoteattestation_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Intel(R). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | v1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // ConditionType is the type of a QuoteAttestationCondition 25 | type ConditionType string 26 | 27 | // ConditionReason is the shaort machine readable reason for 28 | // the occurred condition. 29 | type ConditionReason string 30 | 31 | // Well-known condition types for certificate requests. 32 | const ( 33 | 34 | // ConditionStatusInit indicates the condition for object status 35 | // has just initiated. This is just to allow manual status patching 36 | // using kubectl, where no attestation-controller is running. 37 | // NOTE: This must be removed in near feature. 38 | ConditionStatusInit ConditionType = "Init" 39 | 40 | // ConditionReady indicates the condition for the request is ready 41 | // This should be set by the attestation-controller upon request has 42 | // been resolved, i.e. either success or failure. 43 | ConditionReady ConditionType = "Ready" 44 | 45 | ReasonTCSReconcile ConditionReason = "TCSReconcile" 46 | ReasonControllerReconcile ConditionReason = "AttestationControllerReconcile" 47 | 48 | // ECDSAQuoteVersion3 indicates the SGX ECDSA quote version 3. This is the only 49 | // supported version by the QVE. 50 | ECDSAQuoteVersion3 = "ECDSA Quote 3" 51 | ) 52 | 53 | // QuoteAttestationRequestType type definition for representing 54 | // the type of attestation request 55 | type QuoteAttestationRequestType string 56 | 57 | const ( 58 | // RequestTypeQuoteAttestation represents the type of request 59 | // is for only quote verification 60 | RequestTypeQuoteAttestation = "QuoteAttestation" 61 | // RequestTypeKeyProvisioning represents the type of request 62 | // is for CA key provisioning where quote verification is a 63 | // pre-requisite 64 | RequestTypeKeyProvisioning = "KeyProvisioning" 65 | ) 66 | 67 | // QuoteAttestationSpec defines the desired state of QuoteAttestation 68 | type QuoteAttestationSpec struct { 69 | // Type represents the type of the request, one of "QuoteAttestation", "KeyProvisioning". 70 | // +kubebuilder:validation:Enum=QuoteAttestation;KeyProvisioning 71 | // +kubebuilder:validation:default=KeyProvisioning 72 | Type QuoteAttestationRequestType `json:"type"` 73 | // Quote to be verified, base64-encoded. 74 | // +kubebuilder:listType=atomic 75 | Quote []byte `json:"quote"` 76 | 77 | // QuoteVersion used to for generated quote, default is ECDSA quote "3" 78 | // +kubebuilder:optional 79 | QuoteVersion string `json:"quoteVersion,omitempty"` 80 | 81 | // ServiceID holds the unique identifier(name?) that represents service 82 | // which is requesting for the secret. 83 | // To be decided whether this should be SPIFFE trust domain! 84 | ServiceID string `json:"serviceId"` 85 | 86 | // PublicKey for encrypting the secret, hash is part of the quote data, 87 | // base-64 encoded. 88 | // +kubebuilder:listType=atomic 89 | PublicKey []byte `json:"publicKey"` 90 | 91 | // SignerName refers to the Kubernetes CSR signer name used by 92 | // this request. 93 | SignerName string `json:"signerName"` 94 | 95 | // SecretName is name of the Secret object (in the same namespace) 96 | // to keep the wrapped on secrets (only needed for KeyProvisioning request type) 97 | // which is an opaque type. The secret data must contain two map elements `tls.key` 98 | // and `tls.cert` and the values are the base64 encoded encrypted CA key and 99 | // base64 encoded x509(PEM encoded) certificate. This must be added only after 100 | // a successful quote validation and before updating the status condition. 101 | // +optional 102 | SecretName string `json:"secretName,omitempty"` 103 | } 104 | 105 | // QuoteAttestationCondition describes a condition of a QuoteAttestation object 106 | type QuoteAttestationCondition struct { 107 | // type of the condition. One of QuoteVerified, CASecretReady and Ready 108 | Type ConditionType `json:"type,omitempty"` 109 | // Status indicates the status of a condition (true, false, or unknown). 110 | Status v1.ConditionStatus `json:"status,omitempty"` 111 | // Reason indicates current request state 112 | // +optional 113 | Reason ConditionReason `json:"reason,omitempty"` 114 | // message contains a human readable message with details about the request state 115 | // +optional 116 | Message string `json:"message,omitempty"` 117 | // lastUpdateTime is the time of the last update to this condition 118 | // +optional 119 | LastUpdateTime metav1.Time `json:"lastUpdateTime,omitempty"` 120 | } 121 | 122 | // QuoteAttestationStatus defines the observed state of QuoteAttestation 123 | type QuoteAttestationStatus struct { 124 | // conditions applied to the request. Known conditions are "QuoteVerified", 125 | // "CASecretsReady" and "Ready". 126 | // +optional 127 | Conditions []QuoteAttestationCondition `json:"conditions,omitempty"` 128 | } 129 | 130 | // +kubebuilder:object:root=true 131 | // +kubebuilder:subresource:status 132 | // +genclient 133 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 134 | // +k8s:openapi-gen=true 135 | 136 | // QuoteAttestation is the Schema for the quoteattestations API 137 | type QuoteAttestation struct { 138 | metav1.TypeMeta `json:",inline"` 139 | metav1.ObjectMeta `json:"metadata,omitempty"` 140 | 141 | Spec QuoteAttestationSpec `json:"spec,omitempty"` 142 | Status QuoteAttestationStatus `json:"status,omitempty"` 143 | } 144 | 145 | // +kubebuilder:object:root=true 146 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 147 | 148 | // QuoteAttestationList contains a list of QuoteAttestation 149 | type QuoteAttestationList struct { 150 | metav1.TypeMeta `json:",inline"` 151 | metav1.ListMeta `json:"metadata,omitempty"` 152 | Items []QuoteAttestation `json:"items"` 153 | } 154 | 155 | func init() { 156 | SchemeBuilder.Register(&QuoteAttestation{}, &QuoteAttestationList{}) 157 | } 158 | 159 | func (qas *QuoteAttestationStatus) SetCondition(t ConditionType, status v1.ConditionStatus, reason ConditionReason, message string) { 160 | cond := QuoteAttestationCondition{ 161 | Type: t, 162 | Status: status, 163 | Reason: reason, 164 | Message: message, 165 | LastUpdateTime: metav1.Now(), 166 | } 167 | for i, c := range qas.Conditions { 168 | if c.Type == t { 169 | qas.Conditions[i] = cond 170 | return 171 | } 172 | } 173 | qas.Conditions = append(qas.Conditions, cond) 174 | } 175 | 176 | func (qas *QuoteAttestationStatus) GetCondition(t ConditionType) *QuoteAttestationCondition { 177 | for _, c := range qas.Conditions { 178 | if c.Type == t { 179 | return &c 180 | } 181 | } 182 | return nil 183 | } 184 | -------------------------------------------------------------------------------- /api/v1alpha2/quoteattestation_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Intel(R). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha2 18 | 19 | import ( 20 | v1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // ConditionType is the type of a QuoteAttestationCondition 25 | type ConditionType string 26 | 27 | // ConditionReason is the short machine readable reason for 28 | // the occurred condition. 29 | type ConditionReason string 30 | 31 | // Well-known condition types for certificate requests. 32 | const ( 33 | 34 | // ConditionStatusInit indicates the condition for object status 35 | // has just initiated. This is just to allow manual status patching 36 | // using kubectl, where no attestation-controller is running. 37 | // NOTE: This must be removed in near feature. 38 | ConditionStatusInit ConditionType = "Init" 39 | 40 | // ConditionReady indicates the condition for the request is ready 41 | // This should be set by the attestation-controller upon request has 42 | // been resolved, i.e. either success or failure. 43 | ConditionReady ConditionType = "Ready" 44 | 45 | ReasonTCSReconcile ConditionReason = "TCSReconcile" 46 | ReasonControllerReconcile ConditionReason = "AttestationControllerReconcile" 47 | 48 | // ECDSAQuoteVersion3 indicates the SGX ECDSA quote version 3. This is the only 49 | // supported version by the QVE. 50 | ECDSAQuoteVersion3 = "ECDSA Quote 3" 51 | ) 52 | 53 | // QuoteAttestationRequestType type definition for representing 54 | // the type of attestation request 55 | type QuoteAttestationRequestType string 56 | 57 | const ( 58 | // RequestTypeQuoteAttestation represents the type of request 59 | // is for only quote verification 60 | RequestTypeQuoteAttestation = "QuoteAttestation" 61 | // RequestTypeKeyProvisioning represents the type of request 62 | // is for CA key provisioning where quote verification is a 63 | // pre-requisite 64 | RequestTypeKeyProvisioning = "KeyProvisioning" 65 | ) 66 | 67 | // QuoteAttestationSpec defines the desired state of QuoteAttestation 68 | type QuoteAttestationSpec struct { 69 | // Type represents the type of the request, one of "QuoteAttestation", "KeyProvisioning". 70 | // +kubebuilder:validation:Enum=QuoteAttestation;KeyProvisioning 71 | // +kubebuilder:validation:default=KeyProvisioning 72 | Type QuoteAttestationRequestType `json:"type"` 73 | // Quote to be verified, base64-encoded. 74 | // +kubebuilder:listType=atomic 75 | Quote []byte `json:"quote"` 76 | 77 | // QuoteVersion used to for generated quote, default is ECDSA quote "3" 78 | // +kubebuilder:optional 79 | QuoteVersion string `json:"quoteVersion,omitempty"` 80 | 81 | // Nonce base64-encoded nonce used for generating the SGX Quote. 82 | // This is required for verifying the provided SGX quote by 83 | // the key server(s). 84 | // +kubebuilder:listType=atomic 85 | Nonce []byte `json:"nonce,omitempty"` 86 | 87 | // ServiceID holds the unique identifier(name?) that represents service 88 | // which is requesting for the secret. 89 | // To be decided whether this should be SPIFFE trust domain! 90 | ServiceID string `json:"serviceId"` 91 | 92 | // PublicKey for encrypting the secret, hash is part of the quote data, 93 | // base-64 encoded. 94 | // +kubebuilder:listType=atomic 95 | PublicKey []byte `json:"publicKey"` 96 | 97 | // SignerName refers to the Kubernetes CSR signer name used by 98 | // this request. 99 | SignerName string `json:"signerName"` 100 | 101 | // SecretName is name of the Secret object (in the same namespace) 102 | // to keep the wrapped on secrets (only needed for KeyProvisioning request type) 103 | // which is an opaque type. The secret data must contain two map elements `tls.key` 104 | // and `tls.cert` and the values are the base64 encoded encrypted CA key and 105 | // base64 encoded x509(PEM encoded) certificate. This must be added only after 106 | // a successful quote validation and before updating the status condition. 107 | // +optional 108 | SecretName string `json:"secretName,omitempty"` 109 | } 110 | 111 | // QuoteAttestationCondition describes a condition of a QuoteAttestation object 112 | type QuoteAttestationCondition struct { 113 | // type of the condition. One of QuoteVerified, CASecretReady and Ready 114 | Type ConditionType `json:"type,omitempty"` 115 | // Status indicates the status of a condition (true, false, or unknown). 116 | Status v1.ConditionStatus `json:"status,omitempty"` 117 | // Reason indicates current request state 118 | // +optional 119 | Reason ConditionReason `json:"reason,omitempty"` 120 | // message contains a human readable message with details about the request state 121 | // +optional 122 | Message string `json:"message,omitempty"` 123 | // lastUpdateTime is the time of the last update to this condition 124 | // +optional 125 | LastUpdateTime metav1.Time `json:"lastUpdateTime,omitempty"` 126 | } 127 | 128 | // QuoteAttestationStatus defines the observed state of QuoteAttestation 129 | type QuoteAttestationStatus struct { 130 | // conditions applied to the request. Known conditions are "QuoteVerified", 131 | // "CASecretsReady" and "Ready". 132 | // +optional 133 | Conditions []QuoteAttestationCondition `json:"conditions,omitempty"` 134 | } 135 | 136 | // +genclient 137 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 138 | // +k8s:openapi-gen=true 139 | // +kubebuilder:object:root=true 140 | // +kubebuilder:subresource:status 141 | // +kubebuilder:storageversion 142 | 143 | // QuoteAttestation is the Schema for the quote attestation API 144 | type QuoteAttestation struct { 145 | metav1.TypeMeta `json:",inline"` 146 | metav1.ObjectMeta `json:"metadata,omitempty"` 147 | 148 | Spec QuoteAttestationSpec `json:"spec,omitempty"` 149 | Status QuoteAttestationStatus `json:"status,omitempty"` 150 | } 151 | 152 | //+kubebuilder:object:root=true 153 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 154 | 155 | // QuoteAttestationList contains a list of QuoteAttestation 156 | type QuoteAttestationList struct { 157 | metav1.TypeMeta `json:",inline"` 158 | metav1.ListMeta `json:"metadata,omitempty"` 159 | Items []QuoteAttestation `json:"items"` 160 | } 161 | 162 | func init() { 163 | SchemeBuilder.Register(&QuoteAttestation{}, &QuoteAttestationList{}) 164 | } 165 | 166 | func (qas *QuoteAttestationStatus) SetCondition(t ConditionType, status v1.ConditionStatus, reason ConditionReason, message string) { 167 | cond := QuoteAttestationCondition{ 168 | Type: t, 169 | Status: status, 170 | Reason: reason, 171 | Message: message, 172 | LastUpdateTime: metav1.Now(), 173 | } 174 | for i, c := range qas.Conditions { 175 | if c.Type == t { 176 | qas.Conditions[i] = cond 177 | return 178 | } 179 | } 180 | qas.Conditions = append(qas.Conditions, cond) 181 | } 182 | 183 | func (qas *QuoteAttestationStatus) GetCondition(t ConditionType) *QuoteAttestationCondition { 184 | for _, c := range qas.Conditions { 185 | if c.Type == t { 186 | return &c 187 | } 188 | } 189 | return nil 190 | } 191 | -------------------------------------------------------------------------------- /docs/integrate-key-server.md: -------------------------------------------------------------------------------- 1 | 2 | ## Remote attestation and key management (manual) 3 | 4 | Trusted Certificate Service (TCS) supports SGX remote attestation and sample key management reference application. 5 | 6 | [Remote attestation](https://www.intel.com/content/www/us/en/developer/tools/software-guard-extensions/attestation-services.html) is an advanced feature which allows an entity to gain relying party's trust. Remote attestation gives the relying party increased confidence that the software is running inside a SGX enclave. The attestation results includes the identity of the software being attested and an assesment of possible software tampering. 7 | 8 | Key management enables external key management systems' to deliver the certificates and keys via secure mechanisms into the SGX enclave. 9 | 10 | **NOTE**: In this release we only support manual operations which are for demonstration purposes only. In the future releases will add more capabilities to the attestation and key management. 11 | 12 | The core mechanism to integrate the attestation and key management is a Kubernetes Custom Resource Definition (CRD) `QuoteAttestation`, which is based on [SGX ECDSA attestation](https://www.intel.com/content/www/us/en/developer/articles/technical/quote-verification-attestation-with-intel-sgx-dcap.html) defined by the [Intel® SGX Data Center Attestation Primitives](https://github.com/intel/SGXDataCenterAttestationPrimitives). 13 | 14 | [Intel® KMRA](https://01.org/key-management-reference-application-kmra) project provides command line tools which can read, write and update the `QuoteAttestation`. The KMRA tools also do the attestation and key management based on the information from the `QuoteAttestation`. 15 | 16 | Refer to [QuoteAttestation CRD API](./quote-attestation-api.md) for further details. 17 | 18 | **NOTE**: The cluster admins must regulate the access to the `QuoteAttestation` resource with appropriate [Kubernetes RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) (Role Based Access Control) rules such that no other component in the cluster can create/write/update/delete the `QuoteAttestation` object other than, the Trusted Certificate Service (`tcs-issuer` Kubernetes pod). 19 | 20 | ## Prerequisites for running this example: 21 | 22 | - istioctl 23 | - Kubernetes cluster with at least one Intel SGX enabled node 24 | - AESMD running on the host 25 | 26 | ## Deployment 27 | 28 | Deploy TCS and custom resource definitions (CRDs). 29 | 30 | ```sh 31 | kubectl apply -f deployment/crds/ 32 | kubectl apply -f deployment/tcs_issuer.yaml 33 | ``` 34 | ## Create (non-self-signed) issuer 35 | 36 | Ensure that the `spec.selfSign` of the issuer set to `false` indicating that TCS will not create self-signed certificate but expects it to be provided via other mechanism. 37 | 38 | ```sh 39 | kubectl create ns sandbox 40 | cat < /tmp/public.key 73 | kubectl get quoteattestations.tcs.intel.com -n tcs-issuer sandbox.external-ca.tcsissuer.tcs.intel.com -o jsonpath='{.spec.quote}' | base64 -d > /tmp/quote.data 74 | ``` 75 | 76 | The next command (`km-attest`) needs to be executed on a machine with SGX in order to succeed. 77 | Use `km-attest` tool to do the SGX quote attestation using the public key and quote from the 78 | previous step: 79 | 80 | ```sh 81 | km-attest --pubkey /tmp/public.key --quote /tmp/quote.data 82 | ```` 83 | 84 | Successful attestation looks like this: 85 | 86 | ```console 87 | Public key hash verification successful 88 | SGX_QL_QV_RESULT_OK 89 | Quote is correct, platform contains latest TCB. 90 | Quote verification successful 91 | ``` 92 | 93 | In case you don't have the private key and certificate you can generate one with the following command: 94 | 95 | ```sh 96 | openssl req -x509 -nodes -newkey rsa:4096 -keyout /tmp/ca-private.pem -out /tmp/ca-cert.pem -sha256 -days 365 -subj '/CN=SGX.intel.com' 97 | ``` 98 | 99 | The private key is next delivered to SGX enclave. The private key is encrypted with temporary AES-256 key which is in turn encrypted with the public key from the quote. The two incredients are put together to form a wrap [3]. Only the SGX enclave holding the private key can open the wrap containing the private key. 100 | 101 | Use the `km-wrap` tool to wrap the private key and store it in `WRAPPED_KEY` environment variable: 102 | 103 | ```sh 104 | WRAPPED_KEY=$(sudo km-wrap --pubkey /tmp/public.key --privkey /tmp/ca-private.pem --pin 123456789 --token SgxOperator --module /usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so) 105 | ``` 106 | 3 [PKCS11 wrapping/unwrapping private keys](http://docs.oasis-open.org/pkcs11/pkcs11-curr/v2.40/csprd02/pkcs11-curr-v2.40-csprd02.html#_Toc387327798) 107 | 108 | Verify that the `WRAPPED_KEY` contains base64 encoded data and not error messages (`echo $WRAPPED_KEY`). 109 | 110 | Next, you need to create kubernetes secret, in the correct namespace, which contains the wrapped private key and certificate: 111 | 112 | ```sh 113 | kubectl create secret generic -n tcs-issuer wrapped-key --from-literal=tls.key=${WRAPPED_KEY} --from-literal=tls.crt=$(base64 -w 0 < /tmp/ca-cert.pem) 114 | ``` 115 | 116 | Finally, you need to update (patch) `external-ca.sandbox.tcsissuer.tcs.intel.com` CR. This step will trigger the TCS to process the updated CR, unwrap the key and store the key into SGX enclave. 117 | 118 | ```sh 119 | kubectl proxy --port=9091 & 120 | PROXY_PID=$! 121 | trap 'kill "$PROXY_PID"' EXIT 122 | #wait for proxy to open 123 | sleep 2 124 | curl --header "Content-Type: application/json-patch+json" --request PATCH \ 125 | --data '[{"op": "add", "path": "/status/secrets", "value": {"tcsissuer.tcs.intel.com/sandbox.external-ca": {"secretName": "wrapped-key", "secretType": "KMRA"}}}, {"op": "add", "path": "/status/conditions/-", "value": {"type": "CASecretReady", "status": "true", "reason": "AttestationControllerReconcile", "message": "Quote verification success"}}]' \ 126 | http://localhost:9091/apis/tcs.intel.com/v1alpha1/namespaces/tcs-issuer/quoteattestations/sandbox.external-ca.tcsissuer.tcs.intel.com/status 127 | ``` 128 | 129 | Once the CA key and certificate are provisioned, the TCS is ready for serving the approved 130 | [CertificateSigningRequest](https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster/#create-a-certificate-signing-request-object-to-send-to-the-kubernetes-api) Kubernetes 131 | resources. It checks if the CSR has `spec.signerName` set to `tcsissuer.tcs.intel.com/sandbox.external-ca`. If the signer name matches, the TCS signs the CSR with the private key stored inside the SGX enclave. The signed certificate is added to the `.status.certificate` of the CSR resource. 132 | -------------------------------------------------------------------------------- /deployment/tcs_issuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: tcs-issuer 6 | name: tcs-issuer 7 | --- 8 | apiVersion: v1 9 | kind: ServiceAccount 10 | metadata: 11 | name: tcs-issuer-serviceaccount 12 | namespace: tcs-issuer 13 | --- 14 | apiVersion: rbac.authorization.k8s.io/v1 15 | kind: Role 16 | metadata: 17 | name: tcs-leader-election-role 18 | namespace: tcs-issuer 19 | rules: 20 | - apiGroups: 21 | - "" 22 | resources: 23 | - configmaps 24 | verbs: 25 | - get 26 | - list 27 | - watch 28 | - create 29 | - update 30 | - patch 31 | - delete 32 | - apiGroups: 33 | - coordination.k8s.io 34 | resources: 35 | - leases 36 | verbs: 37 | - get 38 | - list 39 | - watch 40 | - create 41 | - update 42 | - patch 43 | - delete 44 | - apiGroups: 45 | - "" 46 | resources: 47 | - events 48 | verbs: 49 | - create 50 | - patch 51 | --- 52 | apiVersion: rbac.authorization.k8s.io/v1 53 | kind: ClusterRole 54 | metadata: 55 | name: tcs-metrics-reader 56 | rules: 57 | - nonResourceURLs: 58 | - /metrics 59 | verbs: 60 | - get 61 | --- 62 | apiVersion: rbac.authorization.k8s.io/v1 63 | kind: ClusterRole 64 | metadata: 65 | name: tcs-proxy-role 66 | rules: 67 | - apiGroups: 68 | - authentication.k8s.io 69 | resources: 70 | - tokenreviews 71 | verbs: 72 | - create 73 | - apiGroups: 74 | - authorization.k8s.io 75 | resources: 76 | - subjectaccessreviews 77 | verbs: 78 | - create 79 | --- 80 | apiVersion: rbac.authorization.k8s.io/v1 81 | kind: ClusterRole 82 | metadata: 83 | creationTimestamp: null 84 | name: tcs-role 85 | rules: 86 | - apiGroups: 87 | - "" 88 | resources: 89 | - secrets 90 | verbs: 91 | - create 92 | - delete 93 | - get 94 | - list 95 | - patch 96 | - update 97 | - watch 98 | - apiGroups: 99 | - "" 100 | resources: 101 | - secrets/finalizers 102 | verbs: 103 | - get 104 | - patch 105 | - update 106 | - apiGroups: 107 | - cert-manager.io 108 | resources: 109 | - certificaterequests 110 | verbs: 111 | - get 112 | - list 113 | - patch 114 | - update 115 | - watch 116 | - apiGroups: 117 | - cert-manager.io 118 | resources: 119 | - certificaterequests/finalizers 120 | verbs: 121 | - update 122 | - apiGroups: 123 | - cert-manager.io 124 | resources: 125 | - certificaterequests/status 126 | verbs: 127 | - get 128 | - patch 129 | - update 130 | - apiGroups: 131 | - certificates.k8s.io 132 | resources: 133 | - certificatesigningrequests 134 | verbs: 135 | - create 136 | - delete 137 | - get 138 | - list 139 | - patch 140 | - update 141 | - watch 142 | - apiGroups: 143 | - certificates.k8s.io 144 | resources: 145 | - certificatesigningrequests/finalizers 146 | verbs: 147 | - update 148 | - apiGroups: 149 | - certificates.k8s.io 150 | resources: 151 | - certificatesigningrequests/status 152 | verbs: 153 | - get 154 | - patch 155 | - update 156 | - apiGroups: 157 | - certificates.k8s.io 158 | resourceNames: 159 | - tcsclusterissuer.tcs.intel.com/* 160 | - tcsissuer.tcs.intel.com/* 161 | resources: 162 | - signers 163 | verbs: 164 | - sign 165 | - apiGroups: 166 | - tcs.intel.com 167 | resources: 168 | - quoteattestations 169 | verbs: 170 | - create 171 | - delete 172 | - get 173 | - list 174 | - patch 175 | - watch 176 | - apiGroups: 177 | - tcs.intel.com 178 | resources: 179 | - quoteattestations/finalizers 180 | verbs: 181 | - update 182 | - apiGroups: 183 | - tcs.intel.com 184 | resources: 185 | - quoteattestations/status 186 | verbs: 187 | - get 188 | - patch 189 | - update 190 | - apiGroups: 191 | - tcs.intel.com 192 | resources: 193 | - tcsclusterissuers 194 | - tcsissuers 195 | verbs: 196 | - get 197 | - list 198 | - patch 199 | - update 200 | - watch 201 | - apiGroups: 202 | - tcs.intel.com 203 | resources: 204 | - tcsclusterissuers/status 205 | - tcsissuers/status 206 | verbs: 207 | - get 208 | - patch 209 | - update 210 | --- 211 | apiVersion: rbac.authorization.k8s.io/v1 212 | kind: RoleBinding 213 | metadata: 214 | name: tcs-leader-election-rolebinding 215 | namespace: tcs-issuer 216 | roleRef: 217 | apiGroup: rbac.authorization.k8s.io 218 | kind: Role 219 | name: tcs-leader-election-role 220 | subjects: 221 | - kind: ServiceAccount 222 | name: tcs-issuer-serviceaccount 223 | namespace: tcs-issuer 224 | --- 225 | apiVersion: rbac.authorization.k8s.io/v1 226 | kind: ClusterRoleBinding 227 | metadata: 228 | name: tcs-proxy-rolebinding 229 | roleRef: 230 | apiGroup: rbac.authorization.k8s.io 231 | kind: ClusterRole 232 | name: tcs-proxy-role 233 | subjects: 234 | - kind: ServiceAccount 235 | name: tcs-issuer-serviceaccount 236 | namespace: tcs-issuer 237 | --- 238 | apiVersion: rbac.authorization.k8s.io/v1 239 | kind: ClusterRoleBinding 240 | metadata: 241 | name: tcs-rolebinding 242 | roleRef: 243 | apiGroup: rbac.authorization.k8s.io 244 | kind: ClusterRole 245 | name: tcs-role 246 | subjects: 247 | - kind: ServiceAccount 248 | name: tcs-issuer-serviceaccount 249 | namespace: tcs-issuer 250 | --- 251 | apiVersion: v1 252 | data: 253 | tcs_issuer_config.yaml: | 254 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 255 | kind: ControllerManagerConfig 256 | health: 257 | healthProbeBindAddress: :8083 258 | metrics: 259 | bindAddress: 127.0.0.1:8080 260 | webhook: 261 | port: 9443 262 | leaderElection: 263 | leaderElect: true 264 | resourceName: bb9c3a43.sgx.intel.com 265 | kind: ConfigMap 266 | metadata: 267 | name: tcs-config 268 | namespace: tcs-issuer 269 | --- 270 | apiVersion: v1 271 | data: 272 | config.json: | 273 | eyJ1c2VyUGluIjogIlNwZ2xiMk1Jd2VfZUh1MiIsICJzb1BpbiI6ICJXSXBtQkLJtzY5zo 274 | zTpXMiLCAidG9rZW5MYWJlbCI6ICJTZ3hPcGVyYXRvciJ9 275 | kind: Secret 276 | metadata: 277 | name: tcs-issuer-pkcs11-conf 278 | namespace: tcs-issuer 279 | type: Opaque 280 | --- 281 | apiVersion: v1 282 | kind: Service 283 | metadata: 284 | labels: 285 | control-plane: tcs-issuer 286 | name: tcs-metrics-service 287 | namespace: tcs-issuer 288 | spec: 289 | ports: 290 | - name: https 291 | port: 8443 292 | targetPort: https 293 | selector: 294 | control-plane: tcs-issuer 295 | --- 296 | apiVersion: apps/v1 297 | kind: Deployment 298 | metadata: 299 | labels: 300 | control-plane: tcs-issuer 301 | name: tcs-controller 302 | namespace: tcs-issuer 303 | spec: 304 | replicas: 1 305 | selector: 306 | matchLabels: 307 | control-plane: tcs-issuer 308 | template: 309 | metadata: 310 | annotations: 311 | sgx.intel.com/quote-provider: aesmd 312 | labels: 313 | control-plane: tcs-issuer 314 | spec: 315 | containers: 316 | - args: 317 | - --leader-elect 318 | - --zap-devel 319 | - --zap-log-level=5 320 | - --metrics-bind-address=:8082 321 | - --health-probe-bind-address=:8083 322 | - --hsm-config-file=/etc/hsm/config.json 323 | - --user-pin=$USER_PIN 324 | - --so-pin=$SO_PIN 325 | - --use-random-nonce=true 326 | command: 327 | - /tcs-issuer 328 | image: docker.io/intel/trusted-certificate-issuer:latest 329 | imagePullPolicy: Always 330 | livenessProbe: 331 | httpGet: 332 | path: /healthz 333 | port: 8083 334 | initialDelaySeconds: 10 335 | periodSeconds: 180 336 | name: tcs-issuer 337 | readinessProbe: 338 | httpGet: 339 | path: /readyz 340 | port: 8083 341 | initialDelaySeconds: 10 342 | periodSeconds: 5 343 | resources: 344 | limits: 345 | cpu: 500m 346 | memory: 100Mi 347 | sgx.intel.com/enclave: 1 348 | sgx.intel.com/epc: 512Ki 349 | requests: 350 | cpu: 100m 351 | memory: 20Mi 352 | sgx.intel.com/enclave: 1 353 | sgx.intel.com/epc: 512Ki 354 | securityContext: 355 | allowPrivilegeEscalation: false 356 | capabilities: 357 | drop: 358 | - ALL 359 | readOnlyRootFilesystem: true 360 | runAsNonRoot: true 361 | volumeMounts: 362 | - mountPath: /home/tcs-issuer/tokens 363 | name: tokens-dir 364 | - mountPath: /etc/hsm 365 | name: hsm-config 366 | initContainers: 367 | - command: 368 | - /bin/chown 369 | - 5000:5000 370 | - /home/tcs-issuer/tokens 371 | image: busybox:1.34.1 372 | imagePullPolicy: IfNotPresent 373 | name: init 374 | securityContext: 375 | allowPrivilegeEscalation: false 376 | capabilities: 377 | add: 378 | - CAP_CHOWN 379 | drop: 380 | - ALL 381 | readOnlyRootFilesystem: true 382 | volumeMounts: 383 | - mountPath: /home/tcs-issuer/tokens 384 | name: tokens-dir 385 | serviceAccountName: tcs-issuer-serviceaccount 386 | terminationGracePeriodSeconds: 10 387 | volumes: 388 | - hostPath: 389 | path: /var/lib/tcs-issuer/tokens 390 | type: DirectoryOrCreate 391 | name: tokens-dir 392 | - name: hsm-config 393 | secret: 394 | optional: false 395 | secretName: tcs-issuer-pkcs11-conf 396 | -------------------------------------------------------------------------------- /controllers/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Intel(R). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package controllers 17 | 18 | import ( 19 | "context" 20 | "crypto/x509" 21 | "crypto/x509/pkix" 22 | "encoding/asn1" 23 | "encoding/base64" 24 | "fmt" 25 | "strings" 26 | 27 | tcsapi "github.com/intel/trusted-certificate-issuer/api/v1alpha1" 28 | "github.com/intel/trusted-certificate-issuer/internal/k8sutil" 29 | "github.com/intel/trusted-certificate-issuer/internal/keyprovider" 30 | "github.com/intel/trusted-certificate-issuer/internal/sgxutils" 31 | v1 "k8s.io/api/core/v1" 32 | "k8s.io/apimachinery/pkg/api/errors" 33 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 | "k8s.io/apimachinery/pkg/runtime" 35 | "k8s.io/apimachinery/pkg/runtime/schema" 36 | "k8s.io/apimachinery/pkg/types" 37 | "sigs.k8s.io/controller-runtime/pkg/client" 38 | ) 39 | 40 | type IssuerRef struct { 41 | types.NamespacedName 42 | Kind string 43 | } 44 | 45 | func IssuerSpecAndStatus(issuer client.Object) (*tcsapi.TCSIssuerSpec, *tcsapi.TCSIssuerStatus, error) { 46 | switch t := issuer.(type) { 47 | case *tcsapi.TCSIssuer: 48 | return &t.Spec, &t.Status, nil 49 | case *tcsapi.TCSClusterIssuer: 50 | return &t.Spec, &t.Status, nil 51 | } 52 | return nil, nil, fmt.Errorf("unrecognized issuer type") 53 | } 54 | 55 | func SignerNameForIssuer(issuerGVK schema.GroupVersionKind, name, ns string) string { 56 | if issuerGVK.Kind == "TCSClusterIssuer" { 57 | ns = "" // Ignore namespace for cluster-scoped type 58 | } 59 | signerName := strings.ToLower(issuerGVK.GroupKind().String()) + "/" 60 | if ns != "" { 61 | return signerName + ns + "." + name 62 | } 63 | return signerName + name 64 | } 65 | 66 | func IssuerRefForSignerName(signerName string) *IssuerRef { 67 | parts := strings.SplitN(signerName, "/", 2) 68 | if len(parts) != 2 { 69 | return nil 70 | } 71 | 72 | kindGroup := strings.SplitN(parts[0], ".", 2) 73 | if len(kindGroup) != 2 { 74 | return nil 75 | } 76 | if kindGroup[1] != tcsapi.GroupName { 77 | return nil 78 | } 79 | 80 | issuer := &IssuerRef{} 81 | switch strings.ToLower(kindGroup[0]) { 82 | case "tcsissuer", "tcsissuers": 83 | issuer.Kind = "TCSIssuer" 84 | case "tcsclusterissuer", "tcsclusterissuers": 85 | issuer.Kind = "TCSClusterIssuer" 86 | default: 87 | return nil 88 | } 89 | 90 | nameParts := strings.SplitN(parts[1], ".", 2) 91 | if len(nameParts) == 2 { 92 | issuer.Namespace = nameParts[0] 93 | issuer.Name = nameParts[1] 94 | } else { 95 | issuer.Name = nameParts[0] 96 | } 97 | 98 | return issuer 99 | } 100 | 101 | func GetIssuer(ctx context.Context, 102 | c client.Client, 103 | scheme *runtime.Scheme, 104 | issuerRef *IssuerRef) (client.Object, error) { 105 | typeMeta := metav1.TypeMeta{ 106 | Kind: issuerRef.Kind, 107 | APIVersion: tcsapi.GroupVersion.String(), 108 | } 109 | var issuer client.Object 110 | switch issuerRef.Kind { 111 | case "TCSClusterIssuer": 112 | issuer = &tcsapi.TCSClusterIssuer{TypeMeta: typeMeta} 113 | case "TCSIssuer": 114 | issuer = &tcsapi.TCSIssuer{TypeMeta: typeMeta} 115 | default: 116 | return nil, fmt.Errorf("unknown issuer kind '%s'", issuerRef.Kind) 117 | } 118 | 119 | // Get the Issuer or ClusterIssuer 120 | if err := c.Get(ctx, issuerRef.NamespacedName, issuer); err != nil { 121 | return nil, fmt.Errorf("%w: %v", errGetIssuer, err) 122 | } 123 | 124 | return issuer, nil 125 | } 126 | 127 | var ( 128 | // FIXME (avalluri): These identifiers needs to be in sync with 129 | // values defined in intel/Istio: 130 | // https://github.com/intel/istio/blob/release-1.15-intel/security/pkg/nodeagent/sds/sgxconfig.go#L57-L58 131 | // https://github.com/intel/trusted-certificate-issuer/issues/70 132 | // 133 | // oidQuote represents the ASN.1 OBJECT IDENTIFIER for the SGX quote 134 | // and quote validation result. 135 | oidQuote = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 54392, 5, 1283} 136 | // oidQuotePublicKey represents the ASN.1 OBJECT IDENTIFIER for the 137 | // public key used for generating the SGX quote. 138 | oidQuotePublicKey = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 54392, 5, 1284} 139 | // oidQuoteNonce represents the ASN.1 OBJECT IDENTIFIER for the 140 | // nonce used for generating the SGX quote. 141 | oidQuoteNonce = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 54392, 5, 1547} 142 | ) 143 | 144 | // CSRNeedsQuoteVerification checks if QuoteValidation extension set in the 145 | // given csr 146 | func CSRNeedsQuoteVerification(csr *x509.CertificateRequest) bool { 147 | for _, val := range csr.Extensions { 148 | if val.Id.Equal(oidQuote) { 149 | return true 150 | } 151 | } 152 | return false 153 | } 154 | 155 | // ValidateCSRQuote validates the quote information embedded in the 156 | // given certificate signing request. Returns if any error occur doing so. 157 | // 158 | // When this function returns with retry, means the quote verification is 159 | // is in progress so, recall this method after sometime. Otherwise, the 160 | // verification result is returned. 161 | func ValidateCSRQuote(ctx context.Context, c client.Client, obj client.Object, csr *x509.CertificateRequest, signer string) (verified, retry bool, err error) { 162 | nsName := client.ObjectKey{Name: obj.GetName(), Namespace: obj.GetNamespace()} 163 | if nsName.Namespace == "" { 164 | nsName.Namespace = k8sutil.GetNamespace() 165 | } 166 | qa := &tcsapi.QuoteAttestation{} 167 | if err := c.Get(ctx, nsName, qa); err != nil && errors.IsNotFound(err) { 168 | // means no quoteattestation object, create new one 169 | quoteInfo, err := getQuoteAndPublicKeyFromCSR(csr.Extensions) 170 | if err != nil { 171 | return false, false, fmt.Errorf("incomplete information to verify quote from csr extensions: %v", err) 172 | } 173 | 174 | ownerRef := &metav1.OwnerReference{ 175 | APIVersion: obj.GetObjectKind().GroupVersionKind().GroupVersion().String(), 176 | Kind: obj.GetObjectKind().GroupVersionKind().Kind, 177 | Name: obj.GetName(), 178 | UID: obj.GetUID(), 179 | } 180 | if err := k8sutil.QuoteAttestationDeliver(ctx, c, nsName, tcsapi.RequestTypeQuoteAttestation, signer, quoteInfo, "", ownerRef, nil); err != nil { 181 | return false, true, fmt.Errorf("failed to initiate quote attestation: %v", err) 182 | } 183 | return false, true, nil 184 | } else if err != nil { 185 | return false, true, fmt.Errorf("failed to fetch existing QuoteAttestation object: %v", err) 186 | } 187 | 188 | status := qa.Status.GetCondition(tcsapi.ConditionReady) 189 | if status == nil || status.Status == v1.ConditionUnknown { 190 | // Still quote is verification not verified, retry later 191 | return false, true, nil 192 | } 193 | // Remove quote attestation object 194 | defer c.Delete(context.Background(), qa) 195 | return status.Status == v1.ConditionTrue, false, nil 196 | } 197 | 198 | func getQuoteAndPublicKeyFromCSR(extensions []pkix.Extension) (*keyprovider.QuoteInfo, error) { 199 | decodeExtensionValue := func(value []byte) ([]byte, error) { 200 | strValue := "" 201 | if _, err := asn1.Unmarshal(value, &strValue); err != nil { 202 | return nil, err 203 | } 204 | return base64.StdEncoding.DecodeString(strValue) 205 | } 206 | var quoteInfo keyprovider.QuoteInfo 207 | for _, ext := range extensions { 208 | if ext.Id.Equal(oidQuote) { 209 | quote, err := decodeExtensionValue(ext.Value) 210 | if err != nil { 211 | return nil, fmt.Errorf("failed to unmarshal SGX quote extension value: %v", err) 212 | } 213 | quoteInfo.Quote = quote 214 | } else if ext.Id.Equal(oidQuotePublicKey) { 215 | encPublickey, err := decodeExtensionValue(ext.Value) 216 | if err != nil { 217 | return nil, fmt.Errorf("failed to unmarshal SGX quote extension value: %v", err) 218 | } 219 | key, err := sgxutils.ParseQuotePublickey(encPublickey) 220 | if err != nil { 221 | return nil, fmt.Errorf("failed to parse SGX quote publickey value: %v", err) 222 | } 223 | quoteInfo.PublicKey = key 224 | } else if ext.Id.Equal(oidQuoteNonce) { 225 | nonce, err := decodeExtensionValue(ext.Value) 226 | if err != nil { 227 | return nil, fmt.Errorf("failed to parse SGX quote publickey value: %v", err) 228 | } 229 | quoteInfo.Nonce = nonce 230 | } 231 | } 232 | if quoteInfo.Quote == nil { 233 | return nil, fmt.Errorf("missing quote extension") 234 | } 235 | if quoteInfo.PublicKey == nil { 236 | return nil, fmt.Errorf("missing quote public key extension") 237 | } 238 | if quoteInfo.Nonce == nil { 239 | return nil, fmt.Errorf("missing quote nonce extension") 240 | } 241 | return "eInfo, nil 242 | } 243 | 244 | func GetQuoteVerifiedExtension(message string) (*pkix.Extension, error) { 245 | val := asn1.RawValue{ 246 | Bytes: []byte(message), 247 | Class: asn1.ClassUniversal, 248 | Tag: asn1.TagUTF8String, 249 | } 250 | bs, err := asn1.Marshal(val) 251 | if err != nil { 252 | return nil, fmt.Errorf("failed to marshal the raw values for SGX field: %v", err) 253 | } 254 | return &pkix.Extension{ 255 | Id: oidQuote, 256 | Critical: false, 257 | Value: bs, 258 | }, nil 259 | } 260 | -------------------------------------------------------------------------------- /charts/templates/tcs_issuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: tcs-issuer-serviceaccount 5 | namespace: {{ .Release.Namespace | quote }} 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: Role 9 | metadata: 10 | name: tcs-leader-election-role 11 | namespace: {{ .Release.Namespace | quote }} 12 | rules: 13 | - apiGroups: 14 | - "" 15 | resources: 16 | - configmaps 17 | verbs: 18 | - get 19 | - list 20 | - watch 21 | - create 22 | - update 23 | - patch 24 | - delete 25 | - apiGroups: 26 | - coordination.k8s.io 27 | resources: 28 | - leases 29 | verbs: 30 | - get 31 | - list 32 | - watch 33 | - create 34 | - update 35 | - patch 36 | - delete 37 | - apiGroups: 38 | - "" 39 | resources: 40 | - events 41 | verbs: 42 | - create 43 | - patch 44 | --- 45 | apiVersion: rbac.authorization.k8s.io/v1 46 | kind: ClusterRole 47 | metadata: 48 | name: tcs-metrics-reader 49 | rules: 50 | - nonResourceURLs: 51 | - /metrics 52 | verbs: 53 | - get 54 | --- 55 | apiVersion: rbac.authorization.k8s.io/v1 56 | kind: ClusterRole 57 | metadata: 58 | name: tcs-proxy-role 59 | rules: 60 | - apiGroups: 61 | - authentication.k8s.io 62 | resources: 63 | - tokenreviews 64 | verbs: 65 | - create 66 | - apiGroups: 67 | - authorization.k8s.io 68 | resources: 69 | - subjectaccessreviews 70 | verbs: 71 | - create 72 | --- 73 | apiVersion: rbac.authorization.k8s.io/v1 74 | kind: ClusterRole 75 | metadata: 76 | creationTimestamp: null 77 | name: tcs-role 78 | rules: 79 | - apiGroups: 80 | - '*' 81 | resources: 82 | - secrets 83 | verbs: 84 | - create 85 | - delete 86 | - get 87 | - list 88 | - update 89 | - watch 90 | - apiGroups: 91 | - cert-manager.io 92 | resources: 93 | - certificaterequests 94 | verbs: 95 | - get 96 | - list 97 | - patch 98 | - update 99 | - watch 100 | - apiGroups: 101 | - cert-manager.io 102 | resources: 103 | - certificaterequests/finalizers 104 | verbs: 105 | - update 106 | - apiGroups: 107 | - cert-manager.io 108 | resources: 109 | - certificaterequests/status 110 | verbs: 111 | - get 112 | - patch 113 | - update 114 | - apiGroups: 115 | - certificates.k8s.io 116 | resources: 117 | - certificatesigningrequests 118 | verbs: 119 | - create 120 | - delete 121 | - get 122 | - list 123 | - patch 124 | - update 125 | - watch 126 | - apiGroups: 127 | - certificates.k8s.io 128 | resources: 129 | - certificatesigningrequests/finalizers 130 | verbs: 131 | - update 132 | - apiGroups: 133 | - certificates.k8s.io 134 | resources: 135 | - certificatesigningrequests/status 136 | verbs: 137 | - get 138 | - patch 139 | - update 140 | - apiGroups: 141 | - certificates.k8s.io 142 | resourceNames: 143 | - tcsclusterissuer.tcs.intel.com/* 144 | - tcsissuer.tcs.intel.com/* 145 | resources: 146 | - signers 147 | verbs: 148 | - sign 149 | - apiGroups: 150 | - tcs.intel.com 151 | resources: 152 | - quoteattestations 153 | verbs: 154 | - create 155 | - delete 156 | - get 157 | - list 158 | - patch 159 | - update 160 | - watch 161 | - apiGroups: 162 | - tcs.intel.com 163 | resources: 164 | - quoteattestations/finalizers 165 | verbs: 166 | - update 167 | - apiGroups: 168 | - tcs.intel.com 169 | resources: 170 | - quoteattestations/status 171 | verbs: 172 | - get 173 | - patch 174 | - update 175 | - apiGroups: 176 | - tcs.intel.com 177 | resources: 178 | - tcsclusterissuers 179 | - tcsissuers 180 | verbs: 181 | - get 182 | - list 183 | - patch 184 | - update 185 | - watch 186 | - apiGroups: 187 | - tcs.intel.com 188 | resources: 189 | - tcsclusterissuers/status 190 | - tcsissuers/status 191 | verbs: 192 | - get 193 | - patch 194 | - update 195 | --- 196 | apiVersion: rbac.authorization.k8s.io/v1 197 | kind: RoleBinding 198 | metadata: 199 | name: tcs-leader-election-rolebinding 200 | namespace: {{ .Release.Namespace | quote }} 201 | roleRef: 202 | apiGroup: rbac.authorization.k8s.io 203 | kind: Role 204 | name: tcs-leader-election-role 205 | subjects: 206 | - kind: ServiceAccount 207 | name: tcs-issuer-serviceaccount 208 | namespace: {{ .Release.Namespace | quote }} 209 | --- 210 | apiVersion: rbac.authorization.k8s.io/v1 211 | kind: ClusterRoleBinding 212 | metadata: 213 | name: tcs-proxy-rolebinding 214 | roleRef: 215 | apiGroup: rbac.authorization.k8s.io 216 | kind: ClusterRole 217 | name: tcs-proxy-role 218 | subjects: 219 | - kind: ServiceAccount 220 | name: tcs-issuer-serviceaccount 221 | namespace: {{ .Release.Namespace | quote }} 222 | --- 223 | apiVersion: rbac.authorization.k8s.io/v1 224 | kind: ClusterRoleBinding 225 | metadata: 226 | name: tcs-rolebinding 227 | roleRef: 228 | apiGroup: rbac.authorization.k8s.io 229 | kind: ClusterRole 230 | name: tcs-role 231 | subjects: 232 | - kind: ServiceAccount 233 | name: tcs-issuer-serviceaccount 234 | namespace: {{ .Release.Namespace | quote }} 235 | --- 236 | apiVersion: v1 237 | data: 238 | tcs_issuer_config.yaml: | 239 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 240 | kind: ControllerManagerConfig 241 | health: 242 | healthProbeBindAddress: :8083 243 | metrics: 244 | bindAddress: 127.0.0.1:8080 245 | webhook: 246 | port: 9443 247 | leaderElection: 248 | leaderElect: true 249 | resourceName: bb9c3a43.sgx.intel.com 250 | kind: ConfigMap 251 | metadata: 252 | name: tcs-config 253 | namespace: {{ .Release.Namespace | quote }} 254 | --- 255 | apiVersion: v1 256 | data: 257 | sopin: V0lwbUJCybc2Oc6M06Vz 258 | userpin: U3BnbGIyTUl3ZV9lSHUy 259 | kind: Secret 260 | metadata: 261 | name: tcs-issuer-pkcs11-conf 262 | namespace: {{ .Release.Namespace | quote }} 263 | type: Opaque 264 | --- 265 | apiVersion: v1 266 | kind: Service 267 | metadata: 268 | labels: 269 | control-plane: tcs-issuer 270 | name: tcs-metrics-service 271 | namespace: {{ .Release.Namespace | quote }} 272 | spec: 273 | ports: 274 | - name: https 275 | port: 8443 276 | targetPort: https 277 | selector: 278 | control-plane: tcs-issuer 279 | --- 280 | apiVersion: apps/v1 281 | kind: Deployment 282 | metadata: 283 | labels: 284 | control-plane: tcs-issuer 285 | name: tcs-controller 286 | namespace: {{ .Release.Namespace | quote }} 287 | spec: 288 | replicas: 1 289 | selector: 290 | matchLabels: 291 | control-plane: tcs-issuer 292 | template: 293 | metadata: 294 | annotations: 295 | sgx.intel.com/quote-provider: aesmd 296 | labels: 297 | control-plane: tcs-issuer 298 | spec: 299 | containers: 300 | - args: 301 | - --leader-elect 302 | - --zap-devel 303 | - --zap-log-level=5 304 | - --metrics-bind-address=:8082 305 | - --health-probe-bind-address=:8083 306 | - --user-pin=$USER_PIN 307 | - --so-pin=$SO_PIN 308 | {{- if .Values.controllerExtraArgs }} 309 | {{- with .Values.controllerExtraArgs }} 310 | {{- tpl . $ | trim | indent 8 }} 311 | {{- end }} 312 | {{- end }} 313 | command: 314 | - /tcs-issuer 315 | env: 316 | - name: USER_PIN 317 | valueFrom: 318 | secretKeyRef: 319 | key: userpin 320 | name: tcs-issuer-pkcs11-conf 321 | - name: SO_PIN 322 | valueFrom: 323 | secretKeyRef: 324 | key: sopin 325 | name: tcs-issuer-pkcs11-conf 326 | image: "{{ .Values.image.hub }}/{{ .Values.image.name }}:{{ .Values.image.tag }}" 327 | imagePullPolicy: {{ .Values.image.pullPolicy }} 328 | livenessProbe: 329 | httpGet: 330 | path: /healthz 331 | port: 8083 332 | initialDelaySeconds: 10 333 | periodSeconds: 180 334 | name: tcs-issuer 335 | readinessProbe: 336 | httpGet: 337 | path: /readyz 338 | port: 8083 339 | initialDelaySeconds: 10 340 | periodSeconds: 5 341 | resources: 342 | limits: 343 | cpu: 500m 344 | memory: 100Mi 345 | sgx.intel.com/enclave: 1 346 | sgx.intel.com/epc: 512Ki 347 | requests: 348 | cpu: 100m 349 | memory: 20Mi 350 | sgx.intel.com/enclave: 1 351 | sgx.intel.com/epc: 512Ki 352 | securityContext: 353 | allowPrivilegeEscalation: false 354 | readOnlyRootFilesystem: true 355 | runAsNonRoot: true 356 | capabilities: 357 | drop: [ "ALL" ] 358 | volumeMounts: 359 | - mountPath: /home/tcs-issuer/tokens 360 | name: tokens-dir 361 | initContainers: 362 | - command: 363 | - /bin/chown 364 | - -R 365 | - 5000:5000 366 | - /home/tcs-issuer/tokens 367 | image: busybox:1.34.1 #latest stable version 368 | imagePullPolicy: IfNotPresent 369 | name: init 370 | volumeMounts: 371 | - mountPath: /home/tcs-issuer/tokens 372 | name: tokens-dir 373 | securityContext: 374 | allowPrivilegeEscalation: false 375 | readOnlyRootFilesystem: true 376 | capabilities: 377 | drop: [ "ALL" ] 378 | add: [ "CAP_CHOWN" ] 379 | serviceAccountName: tcs-issuer-serviceaccount 380 | terminationGracePeriodSeconds: 10 381 | volumes: 382 | - hostPath: 383 | path: /var/lib/tcs-issuer/tokens 384 | type: DirectoryOrCreate 385 | name: tokens-dir 386 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Intel Corporation. All Rights Reserved. 2 | # 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 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | ARG SDK_VERSION="2.19.100.3" 16 | ARG DCAP_VERSION="1.16.100.2" 17 | 18 | # Build the manager binary 19 | FROM ubuntu:22.10 as builder 20 | 21 | ARG GO_VERSION="1.20" 22 | ARG SDK_VERSION 23 | ARG DCAP_VERSION 24 | ARG SGX_SDK_INSTALLER=sgx_linux_x64_sdk_${SDK_VERSION}.bin 25 | 26 | ENV DEBIAN_FRONTEND=noninteractive 27 | # SGX prerequisites 28 | # hadolint ignore=DL3005,DL3008 29 | RUN apt-get update \ 30 | && apt-get install --no-install-recommends -y \ 31 | ca-certificates \ 32 | wget \ 33 | linux-tools-generic \ 34 | unzip \ 35 | protobuf-compiler \ 36 | libprotobuf-dev \ 37 | build-essential \ 38 | git \ 39 | gnupg \ 40 | && update-ca-certificates \ 41 | # Add 01.org to apt for SGX packages 42 | # hadolint ignore=DL4006 43 | && echo "deb [arch=amd64] https://download.01.org/intel-sgx/sgx_repo/ubuntu jammy main" >> /etc/apt/sources.list.d/intel-sgx.list \ 44 | && wget -O - https://download.01.org/intel-sgx/sgx_repo/ubuntu/intel-sgx-deb.key | apt-key add - \ 45 | # Install SGX PSW 46 | && apt-get update \ 47 | && apt-get install --no-install-recommends -y \ 48 | libsgx-enclave-common=${SDK_VERSION}-jammy1 \ 49 | libsgx-launch=${SDK_VERSION}-jammy1 \ 50 | libsgx-launch-dev=${SDK_VERSION}-jammy1 \ 51 | libsgx-epid=${SDK_VERSION}-jammy1 \ 52 | libsgx-epid-dev=${SDK_VERSION}-jammy1 \ 53 | libsgx-quote-ex=${SDK_VERSION}-jammy1 \ 54 | libsgx-quote-ex-dev=${SDK_VERSION}-jammy1 \ 55 | libsgx-urts=${SDK_VERSION}-jammy1 \ 56 | libsgx-uae-service=${SDK_VERSION}-jammy1 \ 57 | libsgx-ae-epid=${SDK_VERSION}-jammy1 \ 58 | libsgx-ae-le=${SDK_VERSION}-jammy1 \ 59 | libsgx-ae-pce=${SDK_VERSION}-jammy1 \ 60 | libsgx-ae-qe3=${DCAP_VERSION}-jammy1 \ 61 | libsgx-ae-qve=${DCAP_VERSION}-jammy1 \ 62 | libsgx-dcap-ql=${DCAP_VERSION}-jammy1 \ 63 | libsgx-dcap-ql-dev=${DCAP_VERSION}-jammy1 \ 64 | libsgx-pce-logic=${DCAP_VERSION}-jammy1 \ 65 | libsgx-qe3-logic=${DCAP_VERSION}-jammy1 \ 66 | libsgx-dcap-default-qpl=${DCAP_VERSION}-jammy1 \ 67 | && apt-get clean \ 68 | && ln -s /usr/lib/x86_64-linux-gnu/libsgx_enclave_common.so.1 /usr/lib/x86_64-linux-gnu/libsgx_enclave_common.so 69 | 70 | # SGX SDK is installed in /opt/intel directory. 71 | WORKDIR /opt/intel 72 | 73 | # Install SGX SDK 74 | # hadolint ignore=DL4006 75 | RUN wget -O ${SGX_SDK_INSTALLER} https://download.01.org/intel-sgx/sgx-linux/2.19/distro/ubuntu22.04-server/$SGX_SDK_INSTALLER \ 76 | && chmod +x $SGX_SDK_INSTALLER \ 77 | && echo "yes" | ./$SGX_SDK_INSTALLER \ 78 | && rm $SGX_SDK_INSTALLER \ 79 | && ls -l /opt/intel/ 80 | 81 | # Tag/commit-id/branch to use for bulding CTK 82 | ARG CTK_TAG="master" 83 | 84 | # Intel crypto-api-toolkit prerequisites 85 | #https://github.com/intel/crypto-api-toolkit#software-requirements 86 | RUN set -x && apt-get update \ 87 | && apt-get install --no-install-recommends -y \ 88 | dkms libprotobuf23 autoconf \ 89 | autotools-dev libc6-dev \ 90 | libtool build-essential \ 91 | opensc sudo \ 92 | automake wget \ 93 | && apt-get clean \ 94 | && git clone https://github.com/intel/crypto-api-toolkit.git \ 95 | && (cd /opt/intel/crypto-api-toolkit \ 96 | && git checkout ${CTK_TAG} -b v${CTK_TAG} \ 97 | # disable building tests 98 | && sed -i -e 's;test;;g' ./src/Makefile.am \ 99 | # disable enclave signing inside CTK 100 | && sed -i -e '/libp11SgxEnclave.signed.so/d' ./src/p11/trusted/Makefile.am \ 101 | && ./autogen.sh \ 102 | && ./configure --enable-dcap --with-token-path=/home/tcs-issuer \ 103 | && make && make install) 104 | 105 | # Sign the enclave with custom config. 106 | COPY enclave-config enclave-config 107 | ENV SGX_SIGN=/opt/intel/sgxsdk/bin/x64/sgx_sign 108 | RUN set -x; cd /opt/intel/crypto-api-toolkit/src/p11/trusted \ 109 | && ${SGX_SIGN} gendata -enclave ./.libs/libp11SgxEnclave.so.0.0.0 -out /tmp/libp11SgxEnclave.unsigned -config /opt/intel/enclave-config/p11Enclave.config.xml \ 110 | && /opt/intel/enclave-config/sign-enclave.sh -in /tmp/libp11SgxEnclave.unsigned -out /tmp/libp11SgxEnclave.signature -keyout /opt/intel/enclave-config/enclave-publickey.pem \ 111 | && ${SGX_SIGN} catsig -enclave ./.libs/libp11SgxEnclave.so.0.0.0 \ 112 | -config /opt/intel/enclave-config/p11Enclave.config.xml \ 113 | -sig /tmp/libp11SgxEnclave.signature -key /opt/intel/enclave-config/enclave-publickey.pem \ 114 | -unsigned /tmp/libp11SgxEnclave.unsigned \ 115 | -out /usr/local/lib/libp11SgxEnclave.signed.so \ 116 | && echo "----- Generated signed enclave! ----" 117 | 118 | WORKDIR /workspace 119 | RUN wget -O - https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz | tar -zxf - -C / \ 120 | && mkdir -p /usr/local/bin/ \ 121 | && for i in /go/bin/*; do ln -s $i /usr/local/bin/; done 122 | 123 | # Copy the Go Modules manifests 124 | COPY go.mod go.mod 125 | COPY go.sum go.sum 126 | # Copy the go sources 127 | COPY main.go main.go 128 | COPY internal/ internal/ 129 | COPY controllers/ controllers/ 130 | COPY api/ api/ 131 | COPY vendor/ vendor/ 132 | COPY LICENSE LICENSE 133 | 134 | RUN CGO_ENABLED=1 CGO_LDFLAGS="-L/usr/local/lib" GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a --buildmode=pie -o /manager main.go 135 | RUN mkdir -p /usr/local/share/package-licenses \ 136 | && cp /go/LICENSE /usr/local/share/package-licenses/go.LICENSE \ 137 | && cp LICENSE /usr/local/share/package-licenses/tcs-issuer.LICENSE \ 138 | && cp /opt/intel/crypto-api-toolkit/LICENSE.md /usr/local/share/package-licenses/crypto-api-toolkit.LICENSE 139 | 140 | ### 141 | # Clean runtime image which supposed to 142 | # contain all runtime dependecy packages 143 | ### 144 | FROM ubuntu:22.10 as runtime 145 | 146 | ARG SDK_VERSION 147 | ARG DCAP_VERSION 148 | 149 | RUN apt-get update \ 150 | && apt-get install -y wget gnupg \ 151 | && echo "deb [arch=amd64] https://download.01.org/intel-sgx/sgx_repo/ubuntu jammy main" >> /etc/apt/sources.list.d/intel-sgx.list \ 152 | && wget -O - https://download.01.org/intel-sgx/sgx_repo/ubuntu/intel-sgx-deb.key | apt-key add - \ 153 | && sed -i '/deb-src/s/^# //' /etc/apt/sources.list \ 154 | && apt-get update \ 155 | && apt-get remove -y wget gnupg && apt-get autoremove -y \ 156 | && bash -c 'set -o pipefail; apt-get install --no-install-recommends -y \ 157 | libprotobuf23 \ 158 | libsgx-enclave-common=${SDK_VERSION}-jammy1 \ 159 | libsgx-epid=${SDK_VERSION}-jammy1 \ 160 | libsgx-quote-ex=${SDK_VERSION}-jammy1 \ 161 | libsgx-urts=${SDK_VERSION}-jammy1 \ 162 | libsgx-ae-epid=${SDK_VERSION}-jammy1 \ 163 | libsgx-ae-qe3=${DCAP_VERSION}-jammy1 \ 164 | libsgx-dcap-ql=${DCAP_VERSION}-jammy1 \ 165 | libsgx-pce-logic=${DCAP_VERSION}-jammy1 \ 166 | libsgx-qe3-logic=${DCAP_VERSION}-jammy1 \ 167 | libsgx-dcap-default-qpl=${DCAP_VERSION}-jammy1 \ 168 | libsofthsm2 \ 169 | # required for pkcs11-tool 170 | opensc | tee --append /usr/local/share/package-install.log' \ 171 | && rm -rf /var/cache/* \ 172 | && rm -rf /var/log/*log /var/lib/apt/lists/* /var/log/apt/* /var/lib/dpkg/*-old /var/cache/debconf/*-old \ 173 | && ln -s /usr/lib/x86_64-linux-gnu/libsgx_enclave_common.so.1 /usr/lib/x86_64-linux-gnu/libsgx_enclave_common.so 174 | 175 | ### 176 | # Image that downloads the source packages for 177 | # the runtime GPL packages. 178 | ### 179 | FROM ubuntu:22.10 as sources 180 | 181 | COPY --from=runtime /usr/local/share/package-install.log /usr/local/share/package-install.log 182 | COPY --from=runtime /usr/share/doc /tmp/runtime-doc 183 | 184 | RUN sed -i '/deb-src/s/^# //' /etc/apt/sources.list \ 185 | && apt-get update \ 186 | # Install sources of GPL packages 187 | && mkdir /usr/local/share/package-sources && ( cd /usr/local/share/package-sources \ 188 | && grep ^Get: /usr/local/share/package-install.log | grep -v sgx | cut -d ' ' -f 5,7 | \ 189 | while read pkg version; do \ 190 | if ! [ -f /tmp/runtime-doc/$pkg/copyright ]; then \ 191 | echo "ERROR: missing copyright file for $pkg"; \ 192 | fi; \ 193 | if matches=$(grep -w -e MPL -e GPL -e LGPL /tmp/runtime-doc/$pkg/copyright); then \ 194 | echo "INFO: downloading source of $pkg because of the following licenses:"; \ 195 | echo "$matches" | sed -e 's/^/ /'; \ 196 | apt-get source --download-only $pkg=$version || exit 1; \ 197 | else \ 198 | echo "INFO: not downloading source of $pkg, found no copyleft license"; \ 199 | fi; \ 200 | done \ 201 | && apt-get clean) 202 | 203 | ### 204 | # Final trusted-certificate-issuer Image 205 | ### 206 | FROM runtime as final 207 | 208 | WORKDIR / 209 | RUN useradd --create-home --home-dir /home/tcs-issuer --shell /bin/bash --uid 5000 --user-group tcs-issuer 210 | 211 | COPY --from=builder /manager /tcs-issuer 212 | COPY --from=builder /usr/local/lib/libp11* /usr/local/lib/ 213 | COPY --from=builder /opt/intel/enclave-config/enclave-publickey.pem /usr/local/share/enclave-publickey.pem 214 | COPY --from=builder /usr/local/share/package-licenses /usr/local/share/package-licenses 215 | COPY --from=sources /usr/local/share/package-sources /usr/local/share/package-sources 216 | 217 | USER 5000:5000 218 | 219 | ENV LD_LIBRARY_PATH="/usr/local/lib" 220 | 221 | ENTRYPOINT ["/tcs-issuer"] 222 | -------------------------------------------------------------------------------- /controllers/certificate_request_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Intel(R). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "crypto/x509/pkix" 22 | "errors" 23 | "fmt" 24 | "sync" 25 | "time" 26 | 27 | "github.com/go-logr/logr" 28 | tcsapi "github.com/intel/trusted-certificate-issuer/api/v1alpha1" 29 | "github.com/intel/trusted-certificate-issuer/internal/keyprovider" 30 | selfca "github.com/intel/trusted-certificate-issuer/internal/self-ca" 31 | "github.com/intel/trusted-certificate-issuer/internal/tlsutil" 32 | cmutil "github.com/jetstack/cert-manager/pkg/api/util" 33 | cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1" 34 | cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1" 35 | cmpki "github.com/jetstack/cert-manager/pkg/util/pki" 36 | 37 | v1 "k8s.io/api/core/v1" 38 | apierrors "k8s.io/apimachinery/pkg/api/errors" 39 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 40 | "k8s.io/apimachinery/pkg/runtime" 41 | "k8s.io/apimachinery/pkg/types" 42 | ctrl "sigs.k8s.io/controller-runtime" 43 | "sigs.k8s.io/controller-runtime/pkg/client" 44 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 45 | ) 46 | 47 | var ( 48 | errIssuerRef = errors.New("error interpreting issuerRef") 49 | errGetIssuer = errors.New("error getting issuer") 50 | errIssuerNotReady = errors.New("issuer is not ready") 51 | ) 52 | 53 | // CSRReconciler reconciles a CSR object 54 | type CertificateRequestReconciler struct { 55 | client.Client 56 | Log logr.Logger 57 | Scheme *runtime.Scheme 58 | KeyProvider keyprovider.KeyProvider 59 | caProviders map[string]*selfca.CA 60 | mutex sync.Mutex 61 | } 62 | 63 | func NewCertificateRequestReconciler(c client.Client, keyProvider keyprovider.KeyProvider) *CertificateRequestReconciler { 64 | return &CertificateRequestReconciler{ 65 | Log: ctrl.Log.WithName("controllers").WithName("cr"), 66 | Client: c, 67 | KeyProvider: keyProvider, 68 | caProviders: map[string]*selfca.CA{}, 69 | } 70 | } 71 | 72 | //+kubebuilder:rbac:groups=cert-manager.io,resources=certificaterequests,verbs=get;list;watch;update;patch 73 | //+kubebuilder:rbac:groups=cert-manager.io,resources=certificaterequests/status,verbs=get;update;patch 74 | //+kubebuilder:rbac:groups=cert-manager.io,resources=certificaterequests/finalizers,verbs=update 75 | 76 | func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { 77 | if r == nil { 78 | return ctrl.Result{Requeue: false}, fmt.Errorf("nil reconciler") 79 | } 80 | l := r.Log.WithValues("req", req.NamespacedName) 81 | 82 | r.mutex.Lock() 83 | defer r.mutex.Unlock() 84 | 85 | cr := &cmapi.CertificateRequest{} 86 | 87 | if err := r.Get(ctx, req.NamespacedName, cr); err != nil && !apierrors.IsNotFound(err) { 88 | l.V(1).Error(err, "Failed to fetch CSR object") 89 | return ctrl.Result{Requeue: true, RequeueAfter: time.Minute}, err 90 | } 91 | 92 | l.Info("Reconcile") 93 | 94 | patch := client.MergeFrom(cr.DeepCopy()) 95 | 96 | // We now have a cr that belongs to us so we are responsible 97 | // for updating its Ready condition. 98 | setReadyCondition := func(status cmmeta.ConditionStatus, reason, message string) { 99 | cmutil.SetCertificateRequestCondition(cr, cmapi.CertificateRequestConditionReady, status, reason, message) 100 | } 101 | 102 | ignore := false 103 | defer func() { 104 | if ignore { 105 | return 106 | } 107 | if err != nil { 108 | setReadyCondition(cmmeta.ConditionFalse, cmapi.CertificateRequestReasonPending, err.Error()) 109 | // Reset err to nil, otherwise the controller will 110 | // retry the request assuming that reconcile failure. 111 | err = nil 112 | } 113 | l.Info("Updating CR status") 114 | if updateErr := r.Client.Status().Patch(ctx, cr, patch); updateErr != nil { 115 | err = fmt.Errorf("error patching the CertificateRequest status: %v", updateErr) 116 | } 117 | }() 118 | 119 | // Reconciling cr, Original code from: 120 | // https://github.com/cert-manager/sample-external-issuer/blob/main/internal/controllers/certificaterequest_controller.go 121 | switch { 122 | case !cr.DeletionTimestamp.IsZero(): 123 | ignore = true 124 | case cr.Spec.IssuerRef.Group != tcsapi.GroupName: 125 | ignore = true 126 | case cmutil.CertificateRequestHasCondition(cr, cmapi.CertificateRequestCondition{ 127 | Type: cmapi.CertificateRequestConditionReady, 128 | Status: cmmeta.ConditionTrue, 129 | }): 130 | ignore = true 131 | l.Info("cr is Ready. Ignoring.") 132 | case cmutil.CertificateRequestHasCondition(cr, cmapi.CertificateRequestCondition{ 133 | Type: cmapi.CertificateRequestConditionReady, 134 | Status: cmmeta.ConditionFalse, 135 | Reason: cmapi.CertificateRequestReasonDenied, 136 | }): 137 | ignore = true 138 | l.Info("cr is has denied. Ignoring.") 139 | case cmutil.CertificateRequestIsDenied(cr): 140 | l.Info("cr has been denied. Marking as failed.") 141 | 142 | if cr.Status.FailureTime == nil { 143 | nowTime := metav1.Now() 144 | cr.Status.FailureTime = &nowTime 145 | } 146 | 147 | message := "The cr was denied by an approval controller" 148 | setReadyCondition(cmmeta.ConditionFalse, cmapi.CertificateRequestReasonDenied, message) 149 | return ctrl.Result{}, nil 150 | case !cmutil.CertificateRequestIsApproved(cr): 151 | ignore = true 152 | l.Info("cr has not been approved yet. Ignoring.") 153 | return ctrl.Result{}, nil 154 | default: 155 | // Add a Ready condition if one does not already exist 156 | if ready := cmutil.GetCertificateRequestCondition(cr, cmapi.CertificateRequestConditionReady); ready == nil { 157 | l.Info("Initializing Ready condition") 158 | setReadyCondition(cmmeta.ConditionFalse, cmapi.CertificateRequestReasonPending, "Initializing") 159 | return ctrl.Result{}, nil 160 | } 161 | 162 | issuerRef := &IssuerRef{ 163 | NamespacedName: types.NamespacedName{ 164 | Name: cr.Spec.IssuerRef.Name, 165 | Namespace: cr.Namespace, 166 | }, 167 | Kind: cr.Spec.IssuerRef.Kind, 168 | } 169 | 170 | issuer, err := GetIssuer(ctx, r.Client, r.Scheme, issuerRef) 171 | if err != nil { 172 | l.Error(err, "Unable to get the Issuer. Ignoring.") 173 | return ctrl.Result{}, err 174 | } 175 | 176 | _, issuerStatus, err := IssuerSpecAndStatus(issuer) 177 | if err != nil { 178 | l.Error(err, "Unable to get the IssuerStatus. Ignoring.") 179 | return ctrl.Result{}, err 180 | } 181 | 182 | c := issuerStatus.GetCondition(tcsapi.IssuerConditionReady) 183 | if c == nil || c.Status == v1.ConditionFalse { 184 | return ctrl.Result{}, errIssuerNotReady 185 | } 186 | 187 | signerName := SignerNameForIssuer(issuer.GetObjectKind().GroupVersionKind(), issuer.GetName(), issuer.GetNamespace()) 188 | s, err := r.KeyProvider.GetSignerForName(signerName) 189 | if err != nil { 190 | return ctrl.Result{}, fmt.Errorf("failed to get signer for name '%s': %v", signerName, err) 191 | } 192 | 193 | ca, err := selfca.NewCA(s, s.Certificate()) 194 | if err != nil { 195 | return ctrl.Result{}, fmt.Errorf("failed to prepare CA: %v", err) 196 | } 197 | 198 | keyUsage, extKeyUsage, err := cmpki.BuildKeyUsages(cr.Spec.Usages, cr.Spec.IsCA) 199 | if err != nil { 200 | return ctrl.Result{}, fmt.Errorf("key usage error> %v", err) 201 | } 202 | 203 | certRequest, err := tlsutil.DecodeCertRequest(cr.Spec.Request) 204 | if err != nil { 205 | l.Info("Can't decode x509 CSR:", "error", err) 206 | return reconcile.Result{}, nil 207 | } 208 | 209 | csrExtensions := []pkix.Extension{} 210 | if CSRNeedsQuoteVerification(certRequest) { 211 | verified, retry, err := ValidateCSRQuote(ctx, r.Client, cr, certRequest, signerName) 212 | if err != nil { 213 | l.Error(err, "unabled to validate csr quote") 214 | return reconcile.Result{Requeue: retry}, nil 215 | } 216 | if retry { 217 | return reconcile.Result{Requeue: true}, nil 218 | } 219 | if !verified { 220 | cmutil.SetCertificateRequestCondition(cr, cmapi.CertificateRequestConditionDenied, cmmeta.ConditionStatus(v1.ConditionTrue), "Attestation failed", "This request was denied since quote attestation failed") 221 | return ctrl.Result{}, nil 222 | } 223 | verifyExtension, err := GetQuoteVerifiedExtension(ExtensionMessage) 224 | if err != nil { 225 | l.Error(err, "failed to prepare quote verified extension") 226 | return reconcile.Result{}, nil 227 | } 228 | csrExtensions = append(csrExtensions, *verifyExtension) 229 | } 230 | 231 | l.Info("Signing ...") 232 | cert, err := ca.Sign(cr.Spec.Request, keyUsage, extKeyUsage, csrExtensions) 233 | if err != nil { 234 | return ctrl.Result{}, fmt.Errorf("failed to sign CertificateRequest: %v", err) 235 | } 236 | 237 | cr.Status.Certificate = tlsutil.EncodeCert(cert) 238 | cr.Status.CA = ca.EncodedCertificate() 239 | setReadyCondition(cmmeta.ConditionTrue, cmapi.CertificateRequestReasonIssued, "Signed") 240 | l.Info("Signing done") 241 | } 242 | 243 | return ctrl.Result{}, nil 244 | } 245 | 246 | // SetupWithManager sets up the controller with the Manager. 247 | func (r *CertificateRequestReconciler) SetupWithManager(mgr ctrl.Manager) error { 248 | return ctrl.NewControllerManagedBy(mgr). 249 | For(&cmapi.CertificateRequest{ 250 | TypeMeta: metav1.TypeMeta{ 251 | APIVersion: cmapi.SchemeGroupVersion.String(), 252 | Kind: "CertificateRequest", 253 | }, 254 | }). 255 | Complete(r) 256 | } 257 | -------------------------------------------------------------------------------- /controllers/csr_controller_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Intel(R). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers_test 18 | 19 | import ( 20 | "context" 21 | "crypto/rand" 22 | "crypto/rsa" 23 | "crypto/x509/pkix" 24 | "encoding/pem" 25 | "fmt" 26 | "time" 27 | 28 | tcsapi "github.com/intel/trusted-certificate-issuer/api/v1alpha1" 29 | "github.com/intel/trusted-certificate-issuer/controllers" 30 | "github.com/intel/trusted-certificate-issuer/internal/keyprovider" 31 | "github.com/intel/trusted-certificate-issuer/internal/tlsutil" 32 | testutils "github.com/intel/trusted-certificate-issuer/test/utils" 33 | csrv1 "k8s.io/api/certificates/v1" 34 | corev1 "k8s.io/api/core/v1" 35 | v1 "k8s.io/api/core/v1" 36 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 37 | "k8s.io/apimachinery/pkg/types" 38 | "k8s.io/client-go/kubernetes" 39 | 40 | ctrl "sigs.k8s.io/controller-runtime" 41 | "sigs.k8s.io/controller-runtime/pkg/client" 42 | 43 | . "github.com/onsi/ginkgo" 44 | . "github.com/onsi/gomega" 45 | ) 46 | 47 | var _ = Describe("CSR controller", func() { 48 | const ( 49 | testSigner = "tcsissuer.tcs.intel.com/default.test-signer" 50 | unknownSigner = "foo.bar.com/unknown" 51 | ) 52 | 53 | request := newCertificateRequest(nil, "test-service") 54 | type testCase struct { 55 | provisionCA bool 56 | csr *csrv1.CertificateSigningRequest 57 | isApproved bool 58 | isSignerReady bool 59 | 60 | expectedRequeue bool 61 | expectedError error 62 | validateCertificate bool 63 | } 64 | 65 | tests := map[string]testCase{ 66 | "ignore request for unknown signer": { 67 | csr: newCSR("unknown-signer-csr", unknownSigner, request, "", nil), 68 | expectedRequeue: false, 69 | }, 70 | "ignore unapproved request": { 71 | csr: newCSR("pending-csr", testSigner, request, "", nil), 72 | expectedRequeue: false, 73 | }, 74 | "ignore denied request": { 75 | csr: newCSR("denied-csr", testSigner, request, csrv1.CertificateDenied, nil), 76 | expectedRequeue: false, 77 | }, 78 | "shall return appropriate error when signer not ready": { 79 | csr: newCSR("signer-ready-csr", testSigner, request, csrv1.CertificateApproved, nil), 80 | isApproved: true, 81 | isSignerReady: false, 82 | expectedRequeue: true, 83 | expectedError: fmt.Errorf("issuer is not ready"), 84 | }, 85 | "should able to sign certificate": { 86 | provisionCA: true, 87 | csr: newCSR("valid-csr", testSigner, request, csrv1.CertificateApproved, []csrv1.KeyUsage{csrv1.UsageCodeSigning}), 88 | isApproved: true, 89 | isSignerReady: true, 90 | expectedRequeue: false, 91 | validateCertificate: true, 92 | }, 93 | } 94 | 95 | var fakeKeyProvider keyprovider.KeyProvider 96 | var controller *controllers.CSRReconciler 97 | knownSigners := []string{testSigner} 98 | 99 | BeforeEach(func() { 100 | Expect(cfg).ShouldNot(BeNil()) 101 | Expect(k8sClient).ShouldNot(BeNil()) 102 | 103 | fakeKeyProvider = testutils.NewKeyProvider(testutils.Config{ 104 | KnownSigners: knownSigners, 105 | }) 106 | 107 | controller = controllers.NewCSRReconciler(k8sClient, scheme, fakeKeyProvider, false) 108 | }) 109 | 110 | AfterEach(func() { 111 | fakeKeyProvider = nil 112 | controller = nil 113 | for _, name := range knownSigners { 114 | issuer := newIssuer(name) 115 | err := k8sClient.Delete(context.TODO(), issuer) 116 | Expect(err).ShouldNot(HaveOccurred(), "failed to delete issuer") 117 | } 118 | }) 119 | 120 | for name, tc := range tests { 121 | name := name 122 | tc := tc 123 | 124 | It(name, func() { 125 | 126 | for _, name := range knownSigners { 127 | issuer := newIssuer(name) 128 | err := k8sClient.Create(context.TODO(), issuer) 129 | Expect(err).ShouldNot(HaveOccurred(), "failed to create issuer") 130 | 131 | if tc.isSignerReady { 132 | err = k8sClient.Get(context.TODO(), client.ObjectKeyFromObject(issuer), issuer) 133 | Expect(err).ShouldNot(HaveOccurred(), "failed to get issuer") 134 | _, status, _ := controllers.IssuerSpecAndStatus(issuer) 135 | status.SetCondition(tcsapi.IssuerConditionReady, v1.ConditionTrue, "Manual Update", "Updated by unit test") 136 | err = k8sClient.Status().Update(context.TODO(), issuer) 137 | Expect(err).ShouldNot(HaveOccurred(), "failed to update issuer status") 138 | } 139 | } 140 | if tc.provisionCA { 141 | key, err := rsa.GenerateKey(rand.Reader, 3072) 142 | Expect(err).ShouldNot(HaveOccurred(), "failed to create keypair") 143 | 144 | cert, err := testutils.NewCACertificate(key, time.Now(), 365*24*time.Hour, true) 145 | Expect(err).ShouldNot(HaveOccurred(), "failed to create ca certificate") 146 | 147 | _, err = fakeKeyProvider.ProvisionSigner(testSigner, tlsutil.EncodeKey(key), cert) 148 | Expect(err).ShouldNot(HaveOccurred(), "failed to provision key") 149 | } 150 | 151 | err := k8sClient.Create(context.TODO(), tc.csr) 152 | Expect(err).ShouldNot(HaveOccurred(), "failed to create CSR object") 153 | defer k8sClient.Delete(context.TODO(), tc.csr) 154 | 155 | key := types.NamespacedName{ 156 | Name: tc.csr.GetName(), 157 | Namespace: tc.csr.GetNamespace(), 158 | } 159 | 160 | if tc.isApproved { 161 | csr := &csrv1.CertificateSigningRequest{} 162 | err = k8sClient.Get(context.TODO(), client.ObjectKeyFromObject(tc.csr), csr) 163 | Expect(err).ShouldNot(HaveOccurred(), "failed to get csr") 164 | 165 | csr.Status.Conditions = append(csr.Status.Conditions, csrv1.CertificateSigningRequestCondition{ 166 | Type: csrv1.CertificateApproved, 167 | Status: corev1.ConditionTrue, 168 | Reason: "Test Approval", 169 | Message: "This CSR was approved by unit tester", 170 | LastUpdateTime: metav1.Now(), 171 | }) 172 | 173 | // Get CSR client (controller-runtime client does not support CSR approving) 174 | cs, err := kubernetes.NewForConfig(cfg) 175 | Expect(err).ShouldNot(HaveOccurred(), "failed to get client set") 176 | Expect(cs).ShouldNot(BeNil(), "nil client set") 177 | // Approve CSR 178 | _, err = cs.CertificatesV1().CertificateSigningRequests().UpdateApproval(context.TODO(), csr.GetName(), csr, metav1.UpdateOptions{}) 179 | Expect(err).ShouldNot(HaveOccurred(), "failed to update csr approval") 180 | } 181 | 182 | result, err := controller.Reconcile(context.TODO(), ctrl.Request{NamespacedName: key}) 183 | if tc.expectedError == nil { 184 | Expect(err).ShouldNot(HaveOccurred(), "unexpected error") 185 | } else { 186 | Expect(err).Should(HaveOccurred(), "expected an error") 187 | Expect(err.Error()).Should(ContainSubstring(tc.expectedError.Error())) 188 | } 189 | Expect(result.Requeue).Should(BeEquivalentTo(tc.expectedRequeue), "Unexpected result") 190 | 191 | if tc.validateCertificate { 192 | csr := csrv1.CertificateSigningRequest{} 193 | 194 | err := k8sClient.Get(context.TODO(), key, &csr) 195 | Expect(err).ShouldNot(HaveOccurred(), "failed to retrieve CSR") 196 | 197 | Expect(csr.Status.Certificate).ShouldNot(BeNil(), "unexpected nil certificate") 198 | crt, err := tlsutil.DecodeCert(csr.Status.Certificate) 199 | Expect(err).ShouldNot(HaveOccurred(), "failed parse signed certificate") 200 | 201 | s, err := fakeKeyProvider.GetSignerForName(testSigner) 202 | Expect(err).ShouldNot(HaveOccurred(), "failed to get CA signer") 203 | 204 | Expect(crt.Issuer).Should(BeEquivalentTo(s.Certificate().Issuer), "unexpected certificate issuer") 205 | } 206 | }) 207 | } 208 | }) 209 | 210 | func newCSR(name, signerName string, request []byte, condition csrv1.RequestConditionType, usages []csrv1.KeyUsage) *csrv1.CertificateSigningRequest { 211 | if usages == nil { 212 | usages = []csrv1.KeyUsage{csrv1.UsageAny} 213 | } 214 | csr := &csrv1.CertificateSigningRequest{ 215 | ObjectMeta: metav1.ObjectMeta{ 216 | Name: name, 217 | }, 218 | Spec: csrv1.CertificateSigningRequestSpec{ 219 | Request: request, 220 | SignerName: signerName, 221 | Usages: usages, 222 | }, 223 | } 224 | 225 | return csr 226 | } 227 | 228 | func newCertificateRequest(key *rsa.PrivateKey, cn string) []byte { 229 | if key == nil { 230 | var err error 231 | key, err = rsa.GenerateKey(rand.Reader, 1024) 232 | Expect(err).ShouldNot(HaveOccurred(), "failed to create private key") 233 | } 234 | 235 | subj := pkix.Name{ 236 | CommonName: cn, 237 | Organization: []string{"Test Ltd"}, 238 | OrganizationalUnit: []string{"Trusted Certificate Service"}, 239 | } 240 | 241 | csr, err := testutils.NewCertificateRequest(key, subj) 242 | Expect(err).ShouldNot(HaveOccurred(), "create certificate request") 243 | 244 | return pem.EncodeToMemory(&pem.Block{ 245 | Type: "CERTIFICATE REQUEST", 246 | Bytes: csr, 247 | }) 248 | } 249 | 250 | func newIssuer(signerName string) client.Object { 251 | ref := controllers.IssuerRefForSignerName(signerName) 252 | Expect(ref).ShouldNot(BeNil(), "invalid signer name") 253 | 254 | typeMeta := metav1.TypeMeta{ 255 | Kind: ref.Kind, 256 | APIVersion: "tcs.intel.com/v1alpha1", 257 | } 258 | metaData := metav1.ObjectMeta{ 259 | Name: ref.Name, 260 | Namespace: ref.Namespace, 261 | } 262 | 263 | switch ref.Kind { 264 | case "TCSClusterIssuer": 265 | return &tcsapi.TCSClusterIssuer{ 266 | TypeMeta: typeMeta, 267 | ObjectMeta: metaData, 268 | } 269 | case "TCSIssuer": 270 | return &tcsapi.TCSIssuer{ 271 | TypeMeta: typeMeta, 272 | ObjectMeta: metaData, 273 | } 274 | } 275 | Expect(fmt.Errorf("Unexpected kind: %s", ref.Kind)).ShouldNot(HaveOccurred()) 276 | 277 | return nil 278 | } 279 | --------------------------------------------------------------------------------