...
The goal of this requirement is to implement new micro-service called CertService which will request certificates signed by external Certificate Authority (CA) using CMP over HTTP protocol. Uses CMPv2 client to send and receive CMPv2 messages.
CertService's client will be also provided so other ONAP components (aka end components) can easily get certificate from CertService. End component is an ONAP component (e.g. DCAE collector or controller) which requires certificate from CMPv2 server to protect external traffic and uses CertService's client to get it.
...
It is planned that Network Functions (aka xNFs) will get certificates from the same CMPv2 server and the same CA hierarchy, but will use own means to get such certificates. Cause xNFs and ONAP will get certificates signed by the same root CA and will trust such root CA, both parties will automatically trust each other and can communicate with each other.
Context view
Gliffy | ||||||
---|---|---|---|---|---|---|
|
Architecture sketch
Gliffy | ||||||
---|---|---|---|---|---|---|
|
Simplified certificate enrollment flow
Gliffy | ||||||
---|---|---|---|---|---|---|
|
Security considerations
CertService's REST API is protected by mutual HTTPS, meaning server requests client's certificate and authenticate only requests with trusted certificate. After ONAP default installation only certificate from CertService's client is trusted. Authorization isn't supported in Frankfurt release.
...
Method | Endpoint | Parameter | Returned values | ||||||
---|---|---|---|---|---|---|---|---|---|
Name | Is required? | Transfer method | Description | Name | Always returned? | Transfer method | Description | ||
GET | /v1/certificate/{caName} | CA name | Yes | Path parameter | Name of Certificate Authority which should sign sent CSR. Must match CertService's CMPv2 servers configuration. | Error message | No, only if error occurred on server side | Body (JSON) | Verbose information what wrong happened on server side. |
Base64 encoded CSR (Certificate Signing Request) in PEM format | Yes | Header | Certificate Signing Request for given component | Certificate chain | No, only in success case. | Body (JSON) | Base64 encoded signed certificate with whole certificate chain (intermediate CA certificates) in PEM format. Signed certificate should be returned first and then all intermediate certificates in following order: singer of previous certificate till certificate which is signed by root CA. All certificates are in PEM format. | ||
Base64 encoded private key in PEM format | Yes | Header | Private key. Needed to create proof of possession (PoP) | Trusted certificates | No, only in success case. | Body (JSON) | Base64 encoded list of trusted certificates in PEM format. In other words list of root CAs which should be treated as trust anchors. Must contain root CA which was used to sign certificate and may contain other root CAs. Order doesn't matter. All certificates are in PEM format. |
...
HTTP code | Description |
---|---|
200 (OK) | Everything is ok. Certificate chain and trusted certificates returned |
400 (Bad Request) | Incorrect/missing CSR and/or private key |
401 (Unauthorized) | Missing client certificate or presented certificate is not trusted |
404 (Not found) | Invalid CA name in REST API call or wrong endpoint called |
500 (Internal Server Error) | In case of exception on server side. |
OpenAPI
Info |
---|
Swagger will be added here
CMPv2 server properties
...
The best way to open Swagger documentation is to open it in new tab and, when asked, open it in your favorite browser. |
View file | ||||
---|---|---|---|---|
|
CMPv2 server properties
CertService contains configuration of CMPv2 servers. To enroll certificate at least one CMPv2 server has to be configured. CMPv2 servers configuration is read during CertService startup and to take runtime changes into account CertService's refresh configuration endpoint has to be called.
...
Parameter name | Required | Syntax | Description | Validation rules |
---|---|---|---|---|
CA Name | Yes | String (1-128) | The CA name should include the name of the external CA server and the issuerDN, which is the distinguished name of the CA on the external CA server that will sign our certificate. | String (1-128) Should be URL safe as it is used by clients as path parameter in REST calls |
URL | Yes | Schema + IPv4/FQDN + port + path | Url to CMPv2 server; includes mandatory parts: scheme (http://) and IPv4/FQDN and optional parts: port and path (alias); e.g. http://127.0.0.1:8080/pkix or http://127.0.0.1/ejbca/publicweb/cmp/cmp NOTE: If FQDN is given ONAP must be able to resolve it without extra manual configuration | Must be correct URL Must start with http:// scheme If port given, port from 1-65535 range |
Issuer DN | Yes | String (4-256) | Distinguished Name of the CA that will sign the certificate on the CMPv2 server side. When creating an end entity on the external CA server for client mode this IssuerDN will be passed through as the ca to sign for that user. | String (4-256) Correct DN |
CA Mode | Yes | Enum (CLIENT|RA) | Issuer mode (either Registration Authority (RA) or client mode) | Value from predefined set |
Authentication data::IAK | Yes | String (1-256) | Initial authentication key, used, together with RV, to authenticate request in CMPv2 server | String (1-256) |
Authentication data::RV | Yes | String (1-256) | Reference value, used, together with IAK, to authenticate request in CMPv2 server | String (1-256) |
Example
Code Block |
---|
#{ WARNING - work in progress so still can change { "cmpv2Servers":[ { "caName":"TEST", "url":"http://127.0.0.1/ejbca/publicweb/cmp/cmp", "issuerDN":"CN=ManagementCA", "caMode":"CLIENT", "authentication":{ "iak":"xxx", "rv":"yyy" } }, { "caName":"TEST2", "url":"http://127.0.0.1/ejbca/publicweb/cmp/cmpRA", "issuerDN":"CN=ManagementCA2", "caMode":"RA", "authentication":{ "iak":"xxx", "rv":"yyy" } } ] } |
CMPv2 client
Warning |
---|
CMPv2 client exposes only internal API and is called by CertService only. |
Input table for CMPv2 client
CMPv2 will get two POJOs and one String: first with CSR, plain fields extracted from CSR (like subject DN, list of SANs, etc) and private key (in general data passed via REST API call) and second with CMPv2 server details and one String: CA name.
Input value | Input type | DescriptionUsage | |||
---|---|---|---|---|---|
CsrModel | Object | POJO which transfers sent CSR, plain fields extracted from CSR (like Common Name, Country, etc) | |||
CsrModel:: csr | org.bouncycastle.pkcs.PKCS10CertificationRequest | Certificate Signing Request received via REST API | |||
CsrModel:: subjectDN | org.bouncycastle.asn1.x500.X500Name | SubjectDN retrieved from sent CSR | |||
CsrModel:: privateKey | java.security.PrivateKey | Private key received via REST API | |||
CsrModel:: publicKey | java.security.PublicKey | Public key retrieved from sent CSR | |||
CsrModel:: sans | List of Strings | Subject Alterative Names retrieved from sent CSR | CsrModel:: | Others (plain data extracted from sent CSR) if needed | |
CMPv2ServerDetails | Object | POJO which transfers CMPv2 server properties | |||
CMPv2ServerDetails:: CA name | String | CA name as configured in CMPv2 server properties | |||
CMPv2ServerDetails:: URL | URL or String | URL to CMPv2 server as configured in CMPv2 server details | |||
CMPv2ServerDetails:: IssuerDN | org.bouncycastle.asn1.x500.X500Name | Issuer DN as configured in CMPv2 server details | |||
CMPv2ServerDetails:: CA mode | ENUM | CA mode as configured in CMPv2 server details | |||
CMPv2ServerDetails:: IAK | String | IAK as configured in CMPv2 server details | |||
CMPv2ServerDetails:: RV | String | RV as configured in CMPv2 server details | |||
CA name | String | CA name received via REST API |
...
CMPv2 client
...
CMPv2 client returns:
either 3 values:
java.security.cert.X509Certificate endCertificate
List <java.security.cert.X509Certificate> extraCerts
List <java.security.cert.X509Certificate> caPubs
Or 2 values:
List <java.security.cert.X509Certificate> certificateChain
List <java.security.cert.X509Certificate> trustedCerts
CMPv2 client POC
TBD
Currently the POC for CMPv2 client is working based on the inputs below.
...
Input Values
...
Description
...
Usage
...
csrMeta: CA Details
...
.cer file
...
used to validate response (.crt)/ certificate send from EJBCA server
...
Relevant values in Certificate Request message to EJBCA:
...
Value
...
Description
...
Information Included
...
SenderDN
IssuerDN
ProtectionAlgorithm (used for PkiProtection below)
...
CertificateRequestMessage, which includes:
SubjectDN
IssuerDN
SubjectPublicKey
...
Test code for running cmpv2 client against EJBCA server through unit test
...
call to CMPv2 server
Relevant values in Initialization Request (IR) message sent to CMPv2 server:
Value | Description | Information Included |
---|---|---|
PKIHeader | Contains information common to many PKI messages. | SenderDN IssuerDN ProtectionAlgorithm (used for PkiProtection below) |
PKIBody | Contains message-specific information ie. initialization request message | CertificateRequestMessage, which includes: SubjectDN IssuerDN SubjectPublicKey |
PKIProtection | Contains bits that protect PKImessage (Specifically the iak/rv) |
Return values from CMPv2 client
Following table represents return values from CMPv2 client.
Output value | Output type | Description |
---|---|---|
certificateChain | List <java.security.cert.X509Certificate> | Enrolled certificate with full certificate chain (all certificates of intermediate CAs), without root CA |
trustedCerts | List <java.security.cert.X509Certificate> | All trusted certificates returned from CMPv2 server, including root CA |
Test code for running cmpv2 client against EJBCA server through unit test
Code Block |
---|
@Test public void testServerWithRealUrl() throws CmpClientException { setValidCsrMetaValuesAndDateValues(); csrMeta.caUrl("http://127.0.0.1/ejbca/publicweb/cmp/cmpRA"); csrMeta.password("mypassword"); CmpClientImpl cmpClient = new CmpClientImpl(HttpClients.createDefault()); cmpClient.createCertificate("data", "RA", csrMeta, cert, notBefore, notAfter); } private void setValidCsrMetaValuesAndDateValues() { ArrayList<RDN> rdns = new ArrayList<>(); try { rdns.add(new RDN("O=CommonCompany")); } catch (CertException e) { e.printStackTrace(); } csrMeta = new CSRMeta(rdns); csrMeta.cn("CN=CommonName"); csrMeta.san("CommonName.com"); csrMeta.password("password"); csrMeta.email("CommonName@cn.com"); csrMeta.issuerCn("CN=ManagementCA"); when(kpg.generateKeyPair()).thenReturn(keyPair); csrMeta.keypair(); csrMeta.caUrl("http://127.0.0.1/ejbca/publicweb/cmp/cmpSubRAcmp"); csrMeta.password("mypassword"); try { CmpClientImpl cmpClient = new CmpClientImpl(HttpClients.createDefault()); notBefore = cmpClient.createCertificate("data", "RA", csrMeta, cert, notBefore, notAfter); } private void setValidCsrMetaValuesAndDateValues() {new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse("2019/11/11 12:00:00"); ArrayList<RDN> rdnsnotAfter = new ArrayList<>(); try { rdns.add(new RDN("O=CommonCompany")new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse("2020/11/11 12:00:00"); } catch (CertExceptionParseException e) { e.printStackTrace(); } } |
Usage
Docker
Run CertService as docker via following command:
Code Block |
---|
docker run -p $PORT_MAPPING csrMeta = new CSRMeta(rdns); csrMeta.cn("CN=CommonName"); csrMeta.san("CommonName.com"); csrMeta.password("password"); csrMeta.email("CommonName@cn.com"); csrMeta.issuerCn("CN=ManagementCA"); when(kpg.generateKeyPair()).thenReturn(keyPair); csrMeta.keypair(); csrMeta.caUrl("http://127.0.0.1/ejbca/publicweb/cmp/cmp"); try { notBefore = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse("2019/11/11 12:00:00"); notAfter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse("2020/11/11 12:00:00"); } catch (ParseException e) { e.printStackTrace(); } } |
Usage
Docker
Run CertService as docker via following command: TBA
Code Block |
---|
docker run -p 8080:8080 --name cert-service $IMAGE_NAME
|
Kubernetes
For Kubernetes helm chart is provided. Just overwrite needed values and deploy helm chart using following command: TBA
Code Block |
---|
helm install --name $NAME --namespace onap $PATH_TO_HELM_CHART --values $PATH_TO_OVERRIDDEN_VALUES |
CertService's client
CertService's client input properties
...
URL to Cert Service. Default value will be aligned with ONAP K8s deployment (Cert Service's K8s service name and port). Needs to be changed for plain docker deployment.
...
Int (0-120000)
...
Syntax column
Path is valid *inx path
...
Syntax column
Must contain only alphanumeric characters
...
CSR details
...
Syntax column
CN can't contain (special characters (?, $, % and so on), IP addresses, Port numbers, or "http:// or https://")
...
Syntax column
Organization can't contain invalid characters from list "! @ # $ % ^ * ( ) ~ ? > < / \" (without "")
...
Syntax column
...
String (0-2048)
SAN1[:SAN2]
...
Results
As the successful result of running CertService's client (exitCode = 0) following artifacts are created:
...
In case of error CertService's client returns other exit codes (errorCode != 0).
Usage
Docker
Run CertService's client as docker via following command: TBA
...
Kuberenetes
Cause ONAP is deployed in K8s, CertService's client will be delivered as independent container and should run as init container for end component. Both init container and end component must mount the same volume (persistent or ephemeral) to transfer generated artifacts.
Example
Volume to transfer generated artifacts should be mounted to application container (lines 46-49). Within K8s workload, CertService's client as init container should be added (lines 10-13). All needed ENV variables should be passed to CertService's client (lines 14-36). CertService's client should mount the same volume as application container (lines 37-39). Volume to transfer generated artifacts can be an emptyDir type (lines 51-53).
Code Block | ||
---|---|---|
| ||
... --name $NAME --mount $CMPV2_SERVERS_CONFIGURATION $IMAGE_NAME e.g. docker run -p 8080:8080 --name aaf-certservice-api --mount type=bind,source=/<absolute_path>/cmpServers.json,target=/etc/onap/aaf/certservice/cmpServers.json nexus3.onap.org:10001/onap/org.onap.aaf.certservice.aaf-certservice-api:$VERSION |
Kubernetes
For Kubernetes helm chart is provided. Just overwrite needed values and deploy helm chart using following command:
Code Block |
---|
helm install --name $NAME --namespace onap $PATH_TO_HELM_CHART --values $PATH_TO_OVERRIDDEN_VALUES |
CertService's client
CertService's client input properties
Below table presents all properties which should/can be passed to CertService's client to make a successful call to CertService to enroll certificate.
Parameter name | ENV variable name | Required | Default | Syntax | Validation rules | Description | Origin |
---|---|---|---|---|---|---|---|
Url | REQUEST_URL | No | https://aaf-cert-service-service:8443/v1/certificate/ | URL | Syntax column | URL to Cert Service. Default value will be aligned with ONAP K8s deployment (Cert Service's K8s service name and port). Needs to be changed for plain docker deployment. | Application helm chart |
Timeout | REQUEST_TIMEOUT | No | 30000 | Int (0-120000) | Syntax column | Timeout for REST API calls. In miliseconds. A timeout value of zero is interpreted as an infinite timeout. | Application helm chart |
Path | OUTPUT_PATH | Yes | String (1-256) | Syntax column Path is valid *inx path | Path where client will output generated keystore and truststore. Normally this path should be on a volume which is used to transfer keystore and truststore between CertService's client and end component | Application helm chart | |
CA name | CA_NAME | Yes | String (1-128) | Syntax column Must contain only alphanumeric characters | Name of CA which will enroll certificate. Must be same as configured on server side. Used in REST API calls | OOM global value | |
Common Name | COMMON_NAME | Yes | String (1-256) | Syntax column CN can't contain (special characters (?, $, % and so on), IP addresses, Port numbers, or "http:// or https://") | Common name for which certificate from CMPv2 server should be issued | Application helm chart | |
Organization | ORGANIZATION | Yes | String (1-256) | Syntax column Organization can't contain invalid characters from list "! @ # $ % ^ * ( ) ~ ? > < / \" (without "") | Organization for which certificate from CMPv2 server should be issued | OOM global value | |
Organization Unit | ORGANIZATION_UNIT | No | Not available in generated certificate | String (0-256) | Syntax column | Organization unit for which certificate from CMPv2 server should be issued | OOM global value |
Location | LOCATION | No | Not available in generated certificate | String (0-256) | Syntax column | Location for which certificate from CMPv2 server should be issued | OOM global value |
State | STATE | Yes | String (1-256) | Syntax column | State for which certificate from CMPv2 server should be issued | OOM global value | |
Country | COUNTRY | Yes | String(2) | C must be a 2-character ISO format country code | Country for which certificate from CMPv2 server should be issued | OOM global value | |
SANs | SANS | No | Not available in generated certificate | String (0-2048) SAN1[:SAN2] | Syntax column | Subject Alternative Names (SANs) for which certificate from CMPv2 server should be issued. Colon is used as delimiter, e.g. example.com:example.pl. The only supported type of SANs is DNS domain name. NOTE: starting Honolulu release comma is used as delimiter, e.g. example.com,example.pl. | Application helm chart |
Results
As the successful result of running CertService's client (exitCode = 0) following artifacts are created:
Name | Description |
---|---|
keystore.jks | Keystore with certificate chain saved under 'certificate' alias. Protected by random generated password. Keystore in PKCS#12 format. |
keystore.pass | File with password to keystore. Password should be min. 16 chars long and should contain only alphanumeric characters and special characters like Underscore (_), Dollar ($) and Pound (#). Password should be random. |
truststore.jks | Truststore with all trusted certificates. Protected by random generated password. Every trusted certificate is saved under alias with 'trusted-certificate-' prefix. Truststore in PKCS#12 format. |
truststore.pass | File with password to truststore. Password should be min. 16 chars long and should contain only alphanumeric characters and special characters like Underscore (_), Dollar ($) and Pound (#). Password should be random. |
In case of error CertService's client returns other exit codes (errorCode != 0).
Usage
Docker
Run CertService's client as docker via following command:
Code Block |
---|
AAFCERT_CLIENT_IMAGE=onap/org.onap.aaf.certservice.aaf-certservice-client
DOCKER_ENV_FILE= <path to envfile>
NETWORK_CERT_SERVICE= <docker network of cert service>
docker run --name aaf-certservice-client --env-file $DOCKER_ENV_FILE --network $NETWORK_CERT_SERVICE $AAFCERT_CLIENT_IMAGE |
Kuberenetes
Cause ONAP is deployed in K8s, CertService's client will be delivered as independent container and should run as init container for end component. Both init container and end component must mount the same volume (persistent or ephemeral) to transfer generated artifacts.
Example
Volume to transfer generated artifacts should be mounted to application container (lines 57-61). Within K8s workload, CertService's client as init container should be added conditionally (lines 10-14 and 49). All needed ENV variables should be passed to CertService's client (lines 15-45). CertService's client should mount the same volume as application container (lines 46-48). Volume to transfer generated artifacts can be an emptyDir type (lines 64-67).
Code Block | ||
---|---|---|
| ||
... # WARNING - work in progress so still can change kind: Deployment metadata: ... spec: ... template: ... spec: {{- if .Values.global.cmpv2Enabled }} initContainers: - name: cert-service-client image: {{ .Values.global.repository }}/{{ .Values.global.aaf.certServiceClient.image }} imagePullPolicy: {{ .Values.global.pullPolicy | default .Values.pullPolicy }} env: - name: REQUEST_URL value: {{ .Values.global.aaf.certServiceClient.envVariables.requestURL }} - name: REQUEST_TIMEOUT value: {{ .Values.global.aaf.certServiceClient.envVariables.requestTimeout}} - name: OUTPUT_PATH value: {{ .Values.certificate.outputPath }} - name: CA_NAME value: {{ .Values.global.aaf.certServiceClient.envVariables.caName }} # WARNING - work in progress so still can change kind: Deployment metadata: ... spec: ... template: ... spec:name: COMMON_NAME value: {{ .Values.certificate.commonName }} initContainers: - name: ORGANIZATION - name: cert-service-client imagevalue: {{ .Values.global.csClientRepository }}/{{ .Values.global.csClientImageaaf.certServiceClient.envVariables.cmpv2Organization }} imagePullPolicy - name: {{ .Values.global.pullPolicy | default .Values.pullPolicy }}ORGANIZATION_UNIT value: {{ env:.Values.global.aaf.certServiceClient.envVariables.cmpv2OrganizationalUnit }} - name: REQUEST_URLLOCATION value: {{ .Values.certService.url.global.aaf.certServiceClient.envVariables.cmpv2Location }} - name: REQUEST_TIMEOUTSTATE value: {{ .Values.certService.timeoutglobal.aaf.certServiceClient.envVariables.cmpv2State }} - name: OUTPUT_PATHCOUNTRY value: {{ .Values.certService.outputPath.global.aaf.certServiceClient.envVariables.cmpv2Country }} - name: CA_NAMESANS value: {{ .Values.globalcertificate.certService.caNamesans }} - name: COMMONKEYSTORE_NAMEPATH value: {{ .Values.certService.commonNameglobal.aaf.certServiceClient.envVariables.keystorePath }} - name: ORGANIZATIONKEYSTORE_PASSWORD value: {{ .Values.global.certService.organizationaaf.certServiceClient.envVariables.keystorePassword }} - name: ORGANIZATIONTRUSTSTORE_UNITPATH value: {{ .Values.global.aaf.certServicecertServiceClient.envVariables.organizationUnittruststorePath }} - name: LOCATIONTRUSTSTORE_PASSWORD value: {{ .Values.global.aaf.certServicecertServiceClient.envVariables.locationtruststorePassword }} - namevolumeMounts: STATE - valuemountPath: {{ .Values.global.certService.stateoutputPath }} - name: COUNTRY{{ include "common.fullname" . }}-cmpv2-certs value: {{ .Values.global.certService.country end -}} containers: - name: SANS{{ include "common.name" . }} value: image: "{{ include "common.repository" . }}/{{ .Values.certService.sansimage }}" volumeMountsimagePullPolicy: {{ .Values.global.pullPolicy | default .Values.pullPolicy }} - mountPath: {{ .Values.certService.outputPath }} resources: name: {{ include "common.fullnameresources" . | indent 12 }}-cmpv2-certs containersvolumeMounts: - name: {{- includeif "common.name" ..Values.global.cmpv2Enabled }} image: "{{ include "common.repository" . }}/{{ .Values.image }}" - mountPath: /certificates/external imagePullPolicyname: {{ include "common.Values.global.pullPolicy | default .Values.pullPolicy }}fullname" . }}-cmpv2-certs resources: {{ include "common.resources" . | indent 12 readOnly: true {{ end -}} volumeMounts:... volumes: - mountPath: /certificates/external {{- if .Values.global.cmpv2Enabled }} - name: {{ include "common.fullname" . }}-cmpv2-certs readOnlyemptyDir: true{} {{ end -}} |
CMPv2 server
For testing purpose EJBCA is set up.
...
CMPv2 server
For testing purpose EJBCA is set up. It is configured with 2 layer CA hierarchy (root CA and intermediate CA).
EJBCA Setup Script
...
It is configured with 1 layer CA hierarchy (root CA only).
EJBCA Setup Script
View file | ||||
---|---|---|---|---|
|
Future enhancements
- Keep CMPv2 server details in e.g. ESR
Support configurable output type to output artifacts in desired format: JKS, P12 or PEM- implemented in Guilin release- Certificate update implementation on server and client side
- Add to CertService new endpoint to call certificate update in CMPv2 server
- Adjust CertService's client to work as sidecar, not init container
CerService's client adjusted to run in loop (e.g. keep watermark and read it in every run)
- If certificate is not enrolled - request certificate
- If certificate is already enrolled - request certificate update
- Application adjusted to reload keystore/truststore in the runtime or if such is not possible, adjusted to restart itself or inform K8s via probes mechanism that restart is required
- Adjust logging to ONAP guidance
- CMPv2 over HTTPS support