Skip to content

Commit

Permalink
Merge pull request #924 from rszwajko/fetchCert
Browse files Browse the repository at this point in the history
🐾 Fetch and verify certificate from tls-certificate endpoint
  • Loading branch information
yaacov authored Feb 16, 2024
2 parents 4409f9b + a8a5e16 commit db82bde
Show file tree
Hide file tree
Showing 14 changed files with 369 additions and 17 deletions.
14 changes: 10 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@
"Cannot delete migration plan": "Cannot delete migration plan",
"Cannot delete network mapping": "Cannot delete network mapping",
"Cannot delete storage mapping": "Cannot delete storage mapping",
"Cannot retrieve certificate": "Cannot retrieve certificate",
"Category": "Category",
"Certificate change detected": "Certificate change detected",
"Clear all filters": "Clear all filters",
"Click the update credentials button to save your changes, button is disabled until a change is detected.": "Click the update credentials button to save your changes, button is disabled until a change is detected.",
"Close": "Close",
Expand All @@ -60,6 +62,7 @@
"Concern": "Concern",
"Concerns": "Concerns",
"Conditions": "Conditions",
"Confirm": "Confirm",
"Connection Failed": "Connection Failed",
"Controller CPU limit": "Controller CPU limit",
"Controller Memory limit": "Controller Memory limit",
Expand Down Expand Up @@ -100,6 +103,7 @@
"Determines the frequency with which the system checks the status of snapshot creation or removal during oVirt warm migration. The default value is 10 seconds.": "Determines the frequency with which the system checks the status of snapshot creation or removal during oVirt warm migration. The default value is 10 seconds.",
"Domain": "Domain",
"Domain name": "Domain name",
"Drag and drop a file or upload one": "Drag and drop a file or upload one",
"Duplicate": "Duplicate",
"Edit": "Edit",
"Edit Controller CPU limit": "Edit Controller CPU limit",
Expand Down Expand Up @@ -130,9 +134,11 @@
"Error: The format of the provided CA certificate is invalid. Ensure the CA certificate format is valid.": "Error: The format of the provided CA certificate is invalid. Ensure the CA certificate format is valid.",
"Error: this field must be set to a boolean value.": "Error: this field must be set to a boolean value.",
"ESXi": "ESXi",
"Expiration date": "Expiration date",
"Failed": "Failed",
"False": "False",
"Features": "Features",
"Fetch certificate from URL": "Fetch certificate from URL",
"Filter by cluster": "Filter by cluster",
"Filter by endpoint": "Filter by endpoint",
"Filter by features": "Filter by features",
Expand All @@ -159,6 +165,7 @@
"Host cluster": "Host cluster",
"Hosts": "Hosts",
"How to create a migration plan": "How to create a migration plan",
"I trust the authenticity of this certificate": "I trust the authenticity of this certificate",
"If true, the provider's CA certificate won't be validated.": "If true, the provider's CA certificate won't be validated.",
"If true, the provider's TLS certificate won't be validated.": "If true, the provider's TLS certificate won't be validated.",
"Image": "Image",
Expand All @@ -168,8 +175,10 @@
"Invalid username": "Invalid username",
"Inventory": "Inventory",
"Inventory server is not reachable. To troubleshoot, check the Forklift controller pod logs.": "Inventory server is not reachable. To troubleshoot, check the Forklift controller pod logs.",
"Issuer": "Issuer",
"List of objects depended by this object. If ALL objects in the list have been deleted,\n this object will be garbage collected. If this object is managed by a controller,\n then an entry in this list will point to this controller, with the controller field set to true.\n There cannot be more than one managing controller.": "List of objects depended by this object. If ALL objects in the list have been deleted,\n this object will be garbage collected. If this object is managed by a controller,\n then an entry in this list will point to this controller, with the controller field set to true.\n There cannot be more than one managing controller.",
"Loading": "Loading",
"Loading...": "Loading...",
"Manage Columns": "Manage Columns",
"managed": "managed",
"Managed": "Managed",
Expand Down Expand Up @@ -336,6 +345,7 @@
"Sets the maximum number of VMs that can be migrated simultaneously. The default value is 20 virtual machines.": "Sets the maximum number of VMs that can be migrated simultaneously. The default value is 20 virtual machines.",
"Sets the memory limits allocated to the main container in the controller pod. The default value is 800Mi.": "Sets the memory limits allocated to the main container in the controller pod. The default value is 800Mi.",
"Settings": "Settings",
"SHA-1 fingerprint": "SHA-1 fingerprint",
"Show archived": "Show archived",
"Show managed": "Show managed",
"Show the welcome card": "Show the welcome card",
Expand Down Expand Up @@ -366,7 +376,9 @@
"Target provider": "Target provider",
"Template": "Template",
"Tenant": "Tenant",
"The certificate is not a valid PEM-encoded X.509 certificate": "The certificate is not a valid PEM-encoded X.509 certificate",
"The chosen provider is no longer available.": "The chosen provider is no longer available.",
"The current certificate does not match the certificate fetched from URL. Manually validate the fingerprint before proceeding.": "The current certificate does not match the certificate fetched from URL. Manually validate the fingerprint before proceeding.",
"The interval in minutes for precopy. Default value is 60.": "The interval in minutes for precopy. Default value is 60.",
"The interval in seconds for snapshot pooling. Default value is 10.": "The interval in seconds for snapshot pooling. Default value is 10.",
"The limit for CPU usage by the controller, specified in milliCPU. Default value is 500m.": "The limit for CPU usage by the controller, specified in milliCPU. Default value is 500m.",
Expand Down Expand Up @@ -405,6 +417,7 @@
"Unsupported provider type": "Unsupported provider type",
"Update credentials": "Update credentials",
"Updated": "Updated",
"Upload": "Upload",
"URL": "URL",
"URL of the API endpoint of the Red Hat Virtualization Manager (RHVM) on which the source VM is mounted. Ensure that the URL includes the path leading to the RHVM API server, usually /ovirt-engine/api. For example, https://rhv-host-example.com/ovirt-engine/api.": "URL of the API endpoint of the Red Hat Virtualization Manager (RHVM) on which the source VM is mounted. Ensure that the URL includes the path leading to the RHVM API server, usually /ovirt-engine/api. For example, https://rhv-host-example.com/ovirt-engine/api.",
"URL of the NFS file share that serves the OVA., for example, 10.10.0.10:/ova": "URL of the NFS file share that serves the OVA., for example, 10.10.0.10:/ova",
Expand All @@ -416,6 +429,7 @@
"Validation Failed": "Validation Failed",
"vCenter": "vCenter",
"VDDK init image": "VDDK init image",
"Verify certificate": "Verify certificate",
"View details": "View details",
"View provider details": "View provider details",
"Virtual Machine Migrations": "Virtual Machine Migrations",
Expand Down
2 changes: 2 additions & 0 deletions packages/forklift-console-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
"@kubev2v/common": "*",
"@kubev2v/legacy": "*",
"@migtools/lib-ui": "8.4.1",
"@types/jsrsasign": "10.5.12",
"immer": "^10.0.3",
"jsonpath": "^1.1.1",
"jsrsasign": "11.1.0",
"luxon": "^3.3.0",
"msw": "^1.2.3",
"streamsaver": "^2.0.6",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { toColonSeparatedHex } from '../useTlsCertificate';

describe('valid hex to colon separated', () => {
it('works for empty string', () => {
expect(toColonSeparatedHex('')).toBe('');
});
it('works for single letter', () => {
expect(toColonSeparatedHex('a')).toBe('A');
});
it('works for n=2', () => {
expect(toColonSeparatedHex('ab')).toBe('AB');
});
it('works for n=3', () => {
expect(toColonSeparatedHex('abc')).toBe('AB:C');
});
it('works for n=4', () => {
expect(toColonSeparatedHex('abcd')).toBe('AB:CD');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './useK8sWatchSecretData';
export * from './useProviderInventory';
export * from './useProvidersInventoryList';
export * from './useProviderType';
export * from './useTlsCertificate';
export * from './useToggle';
export * from './utils';
// @endindex
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useEffect, useState } from 'react';
import { KJUR, pemtohex, X509, zulutodate } from 'jsrsasign';

import { consoleFetch } from '@openshift-console/dynamic-plugin-sdk';

import { getServicesApiUrl } from '../utils';

/**
* @param value PEM encoded certificate
* @returns parsed certificate or undefined if parsing failed
*/
const parseToX509 = (value: string) => {
if (!value) return undefined;
try {
const cert = new X509();
cert.readCertPEM(value);
return cert;
} catch (e) {
return undefined;
}
};

export const toColonSeparatedHex = (hexString: string) =>
Array.from(hexString.toUpperCase())
// [a,b,c,d] => [[c,d][a,b]]
.reduce(
([last = [], ...rest]: string[][], char: string) =>
last.length != 2 ? [[...last, char], ...rest] : [[char], last, ...rest],
[],
)
// [[c,d][a,b]] => [[a,b], [c,d]]
.reverse()
.map((tuples) => tuples.join(''))
.join(':');

/**
* @param pemEncodedCert valid PEM encoded certificate
* @returns SHA1 thumbprint
*/
export const calculateThumbprint = (pemEncodedCert: string) =>
toColonSeparatedHex(KJUR.crypto.Util.hashHex(pemtohex(pemEncodedCert), 'sha1'));

/**
* @param url URL param for the tls-certificate endpoint
* @returns certificate and props calculated/parsed based on the certificate
*/
export const useTlsCertificate = (url: string) => {
const [certificate, setCertificate] = useState('');
const [fetchError, setFetchError] = useState(false);
const [loading, setLoading] = useState<boolean>(true);

useEffect(() => {
consoleFetch(getServicesApiUrl(`tls-certificate?URL=${url}`), {
method: 'GET',
})
.then((response: Response) => response.text())
.then((certificate) => setCertificate(certificate))
.catch((e) => setFetchError(e))
.then(() => setLoading(false));
}, [url]);

const x509Cert: X509 = parseToX509(certificate);
const certError = !x509Cert && !loading && !fetchError;
const {
thumbprint = '',
issuer = '',
validTo = undefined,
} = x509Cert
? {
thumbprint: calculateThumbprint(certificate),
issuer: KJUR.asn1.x509.X500Name.onelineToLDAP(x509Cert.getIssuerString()),
validTo: zulutodate(x509Cert.getNotAfter()),
}
: {};

return {
loading,
fetchError,
certError,
thumbprint,
issuer,
validTo,
certificate,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.forklift-certificate-upload-margin {
margin-top: 0.5rem;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { FC } from 'react';
import { useModal } from 'src/modules/Providers/modals';
import { useForkliftTranslation } from 'src/utils/i18n';

import { Button, FileUpload, FileUploadProps, Flex, FlexItem } from '@patternfly/react-core';

import { FetchCertificateModal } from './FetchCertificateModal';

import './CertificateUpload.style.css';

export interface CertificateUploadProps extends FileUploadProps {
url?: string;
}

/**
* Provide the certificate using following paths:
* 1. manual copy-paste (provided by FileUpload widget)
* 2. uploading a file (provided by FileUpload widget)
* 3. fetch from the specified URL (via tls-certificate endpoint) end verify
*/
export const CertificateUpload: FC<CertificateUploadProps> = ({
id,
url,
value,
filenamePlaceholder,
browseButtonText,
validated,
onDataChange,
onTextChange,
onClearClick,
isDisabled,
type,
}) => {
const { showModal } = useModal();
const { t } = useForkliftTranslation();
const isText = !type || type === 'text';
return (
<>
<FileUpload
id={id}
type={type || 'text'}
filenamePlaceholder={filenamePlaceholder || t('Drag and drop a file or upload one')}
value={value}
validated={validated}
onDataChange={onDataChange}
onTextChange={onTextChange}
onClearClick={onClearClick}
browseButtonText={browseButtonText || t('Upload')}
isDisabled={isDisabled}
>
{url && isText && (
<Flex>
<FlexItem>
<Button
className="forklift-certificate-upload-margin"
isDisabled={isDisabled}
variant="secondary"
onClick={() =>
showModal(
<FetchCertificateModal
url={url}
handleSave={onTextChange}
existingCert={String(value)}
/>,
)
}
>
{t('Fetch certificate from URL')}
</Button>
</FlexItem>
</Flex>
)}
</FileUpload>
</>
);
};
Loading

0 comments on commit db82bde

Please sign in to comment.