diff --git a/data/init.sql b/data/init.sql index 59a764d89..127ec0bc7 100644 --- a/data/init.sql +++ b/data/init.sql @@ -70,6 +70,7 @@ CREATE TABLE IF NOT EXISTS profiles( kvm_enabled BOOLEAN NULL, sol_enabled BOOLEAN NULL, tls_signing_authority varchar(40) NULL, + tls_signing_authority_url varchar(128) NULL, ieee8021x_profile_name citext, FOREIGN KEY (ieee8021x_profile_name,tenant_id) REFERENCES ieee8021xconfigs(profile_name,tenant_id), PRIMARY KEY (profile_name, tenant_id) diff --git a/src/data/postgres/tables/profiles.test.ts b/src/data/postgres/tables/profiles.test.ts index 292b5b699..fb31a9d57 100644 --- a/src/data/postgres/tables/profiles.test.ts +++ b/src/data/postgres/tables/profiles.test.ts @@ -46,6 +46,7 @@ describe('profiles tests', () => { kvmEnabled: true, solEnabled: true, tlsSigningAuthority: null, + tlsSigningAuthorityURL: null, ieee8021xProfileName: null } as any db = new PostgresDb('') @@ -120,6 +121,7 @@ describe('profiles tests', () => { sol_enabled as "solEnabled", p.tenant_id as "tenantId", tls_signing_authority as "tlsSigningAuthority", + tls_signing_authority_url as "tlsSigningAuthorityURL", p.xmin as "version", ieee8021x_profile_name as "ieee8021xProfileName", COALESCE(json_agg(json_build_object('profileName',wc.wireless_profile_name, 'priority', wc.priority)) FILTER (WHERE wc.wireless_profile_name IS NOT NULL), '[]') AS "wifiConfigs", @@ -142,6 +144,7 @@ describe('profiles tests', () => { kvm_enabled, sol_enabled, tls_signing_authority, + tls_signing_authority_url, p.tenant_id, ieee8021x_profile_name, ip_sync_enabled, @@ -178,6 +181,7 @@ describe('profiles tests', () => { sol_enabled as "solEnabled", p.tenant_id as "tenantId", tls_signing_authority as "tlsSigningAuthority", + tls_signing_authority_url as "tlsSigningAuthorityURL", p.xmin as "version", ieee8021x_profile_name as "ieee8021xProfileName", COALESCE(json_agg(json_build_object('profileName',wc.wireless_profile_name, 'priority', wc.priority)) FILTER (WHERE wc.wireless_profile_name IS NOT NULL), '[]') AS "wifiConfigs", @@ -200,6 +204,7 @@ describe('profiles tests', () => { kvm_enabled, sol_enabled, tls_signing_authority, + tls_signing_authority_url, p.tenant_id, ieee8021x_profile_name, ip_sync_enabled, @@ -285,8 +290,8 @@ describe('profiles tests', () => { mebx_password, generate_random_mebx_password, tags, dhcp_enabled, tls_mode, user_consent, ider_enabled, kvm_enabled, sol_enabled, - tenant_id, tls_signing_authority, ieee8021x_profile_name, ip_sync_enabled, local_wifi_sync_enabled) - values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)`, + tenant_id, tls_signing_authority, tls_signing_authority_url, ieee8021x_profile_name, ip_sync_enabled, local_wifi_sync_enabled) + values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)`, [ amtConfig.profileName, amtConfig.activation, @@ -304,6 +309,7 @@ describe('profiles tests', () => { amtConfig.solEnabled, amtConfig.tenantId, amtConfig.tlsSigningAuthority, + amtConfig.tlsSigningAuthorityURL, amtConfig.ieee8021xProfileName, amtConfig.ipSyncEnabled, amtConfig.localWifiSyncEnabled @@ -335,8 +341,8 @@ describe('profiles tests', () => { mebx_password, generate_random_mebx_password, tags, dhcp_enabled, tls_mode, user_consent, ider_enabled, kvm_enabled, sol_enabled, - tenant_id, tls_signing_authority, ieee8021x_profile_name, ip_sync_enabled, local_wifi_sync_enabled) - values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)`, + tenant_id, tls_signing_authority, tls_signing_authority_url, ieee8021x_profile_name, ip_sync_enabled, local_wifi_sync_enabled) + values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)`, [ amtConfig.profileName, amtConfig.activation, @@ -354,6 +360,7 @@ describe('profiles tests', () => { amtConfig.solEnabled, amtConfig.tenantId, amtConfig.tlsSigningAuthority, + amtConfig.tlsSigningAuthorityURL, amtConfig.ieee8021xProfileName, amtConfig.ipSyncEnabled, amtConfig.localWifiSyncEnabled @@ -397,8 +404,8 @@ describe('profiles tests', () => { mebx_password=$6, generate_random_mebx_password=$7, tags=$8, dhcp_enabled=$9, tls_mode=$10, user_consent=$13, ider_enabled=$14, kvm_enabled=$15, sol_enabled=$16, - tls_signing_authority=$17, ieee8021x_profile_name=$18, - ip_sync_enabled=$19, local_wifi_sync_enabled=$20 + tls_signing_authority=$17, tls_signing_authority_url=$18, ieee8021x_profile_name=$19, + ip_sync_enabled=$20, local_wifi_sync_enabled=$21 WHERE profile_name=$1 and tenant_id = $11 and xmin = $12`, [ amtConfig.profileName, @@ -418,6 +425,7 @@ describe('profiles tests', () => { amtConfig.kvmEnabled, amtConfig.solEnabled, amtConfig.tlsSigningAuthority, + amtConfig.tlsSigningAuthorityURL, amtConfig.ieee8021xProfileName, amtConfig.ipSyncEnabled, amtConfig.localWifiSyncEnabled @@ -442,8 +450,8 @@ describe('profiles tests', () => { mebx_password=$6, generate_random_mebx_password=$7, tags=$8, dhcp_enabled=$9, tls_mode=$10, user_consent=$13, ider_enabled=$14, kvm_enabled=$15, sol_enabled=$16, - tls_signing_authority=$17, ieee8021x_profile_name=$18, - ip_sync_enabled=$19, local_wifi_sync_enabled=$20 + tls_signing_authority=$17, tls_signing_authority_url=$18, ieee8021x_profile_name=$19, + ip_sync_enabled=$20, local_wifi_sync_enabled=$21 WHERE profile_name=$1 and tenant_id = $11 and xmin = $12`, [ amtConfig.profileName, @@ -463,6 +471,7 @@ describe('profiles tests', () => { amtConfig.kvmEnabled, amtConfig.solEnabled, amtConfig.tlsSigningAuthority, + amtConfig.tlsSigningAuthorityURL, amtConfig.ieee8021xProfileName, amtConfig.ipSyncEnabled, amtConfig.localWifiSyncEnabled diff --git a/src/data/postgres/tables/profiles.ts b/src/data/postgres/tables/profiles.ts index f1e36c225..f629886a6 100644 --- a/src/data/postgres/tables/profiles.ts +++ b/src/data/postgres/tables/profiles.ts @@ -74,6 +74,7 @@ export class ProfilesTable implements IProfilesTable { sol_enabled as "solEnabled", p.tenant_id as "tenantId", tls_signing_authority as "tlsSigningAuthority", + tls_signing_authority_url as "tlsSigningAuthorityURL", p.xmin as "version", ieee8021x_profile_name as "ieee8021xProfileName", COALESCE(json_agg(json_build_object('profileName',wc.wireless_profile_name, 'priority', wc.priority)) FILTER (WHERE wc.wireless_profile_name IS NOT NULL), '[]') AS "wifiConfigs", @@ -96,6 +97,7 @@ export class ProfilesTable implements IProfilesTable { kvm_enabled, sol_enabled, tls_signing_authority, + tls_signing_authority_url, p.tenant_id, ieee8021x_profile_name, ip_sync_enabled, @@ -136,6 +138,7 @@ export class ProfilesTable implements IProfilesTable { sol_enabled as "solEnabled", p.tenant_id as "tenantId", tls_signing_authority as "tlsSigningAuthority", + tls_signing_authority_url as "tlsSigningAuthorityURL", p.xmin as "version", ieee8021x_profile_name as "ieee8021xProfileName", COALESCE(json_agg(json_build_object('profileName',wc.wireless_profile_name, 'priority', wc.priority)) FILTER (WHERE wc.wireless_profile_name IS NOT NULL), '[]') AS "wifiConfigs", @@ -158,6 +161,7 @@ export class ProfilesTable implements IProfilesTable { kvm_enabled, sol_enabled, tls_signing_authority, + tls_signing_authority_url, p.tenant_id, ieee8021x_profile_name, ip_sync_enabled, @@ -224,8 +228,8 @@ export class ProfilesTable implements IProfilesTable { mebx_password, generate_random_mebx_password, tags, dhcp_enabled, tls_mode, user_consent, ider_enabled, kvm_enabled, sol_enabled, - tenant_id, tls_signing_authority, ieee8021x_profile_name, ip_sync_enabled, local_wifi_sync_enabled) - values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)`, + tenant_id, tls_signing_authority, tls_signing_authority_url, ieee8021x_profile_name, ip_sync_enabled, local_wifi_sync_enabled) + values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)`, [ amtConfig.profileName, amtConfig.activation, @@ -243,6 +247,7 @@ export class ProfilesTable implements IProfilesTable { amtConfig.solEnabled, amtConfig.tenantId, amtConfig.tlsSigningAuthority, + amtConfig.tlsSigningAuthorityURL, amtConfig.ieee8021xProfileName, amtConfig.ipSyncEnabled, amtConfig.localWifiSyncEnabled @@ -297,8 +302,8 @@ export class ProfilesTable implements IProfilesTable { mebx_password=$6, generate_random_mebx_password=$7, tags=$8, dhcp_enabled=$9, tls_mode=$10, user_consent=$13, ider_enabled=$14, kvm_enabled=$15, sol_enabled=$16, - tls_signing_authority=$17, ieee8021x_profile_name=$18, - ip_sync_enabled=$19, local_wifi_sync_enabled=$20 + tls_signing_authority=$17, tls_signing_authority_url=$18, ieee8021x_profile_name=$19, + ip_sync_enabled=$20, local_wifi_sync_enabled=$21 WHERE profile_name=$1 and tenant_id = $11 and xmin = $12`, [ amtConfig.profileName, @@ -318,6 +323,7 @@ export class ProfilesTable implements IProfilesTable { amtConfig.kvmEnabled, amtConfig.solEnabled, amtConfig.tlsSigningAuthority, + amtConfig.tlsSigningAuthorityURL, amtConfig.ieee8021xProfileName, amtConfig.ipSyncEnabled, amtConfig.localWifiSyncEnabled diff --git a/src/models/RCS.Config.ts b/src/models/RCS.Config.ts index bf976ae6a..6d2e4aea3 100644 --- a/src/models/RCS.Config.ts +++ b/src/models/RCS.Config.ts @@ -288,7 +288,8 @@ export enum TlsMode { export enum TlsSigningAuthority { SELF_SIGNED = 'SelfSigned', - MICROSOFT_CA = 'MicrosoftCA' + MICROSOFT_CA = 'MicrosoftCA', + CUSTOM_CA = 'Chip2Cloud' } export interface connectionParams { diff --git a/src/models/index.ts b/src/models/index.ts index b5e7c08c4..b2a6348b0 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -159,6 +159,7 @@ export class AMTConfiguration { tlsMode?: TlsMode tlsCerts?: TLSCerts tlsSigningAuthority?: TlsSigningAuthority | null + tlsSigningAuthorityURL?: string | null userConsent?: AMTUserConsent | null iderEnabled?: boolean kvmEnabled?: boolean diff --git a/src/routes/admin/profiles/amtProfileValidator.test.ts b/src/routes/admin/profiles/amtProfileValidator.test.ts index 8ab515895..bb9bf3b82 100644 --- a/src/routes/admin/profiles/amtProfileValidator.test.ts +++ b/src/routes/admin/profiles/amtProfileValidator.test.ts @@ -61,6 +61,7 @@ describe('AMT Profile Validation', () => { it('should pass on creation with valid TLS values', async () => { req.body.tlsMode = TlsMode.MUTUAL_ONLY req.body.tlsSigningAuthority = TlsSigningAuthority.MICROSOFT_CA + req.body.tlsSigningAuthorityURL = 'https://www.intel.com' await testExpressValidatorMiddleware(req, res, amtProfileValidator()) const errors = validationResult(req) expect(errors.isEmpty()).toBeTruthy() diff --git a/src/routes/admin/profiles/amtProfileValidator.ts b/src/routes/admin/profiles/amtProfileValidator.ts index 0850b7b04..887a9dabd 100644 --- a/src/routes/admin/profiles/amtProfileValidator.ts +++ b/src/routes/admin/profiles/amtProfileValidator.ts @@ -102,6 +102,10 @@ export const amtProfileValidator = (): ValidationChain[] => [ .optional({ nullable: true }) .isIn(Object.values(TlsSigningAuthority)) .withMessage(`TLS Signing Authority must be one of ${Object.values(TlsSigningAuthority)}`), + check('tlsSigningAuthorityURL') + .optional({ nullable: true }) + .matches('^(?:http[s]?://.)?(?:www.)?[-a-zA-Z0-9@%._+~#=]{2,256}.[a-z]{2,6}(?:[-a-zA-Z0-9@:%_+.~#?&//=]*)$') + .withMessage(`TLS Signing Authority URL must be a valid URL`), check('ciraConfigName') .optional({ nullable: true }) .custom((value, { req }) => { diff --git a/src/routes/admin/profiles/common.ts b/src/routes/admin/profiles/common.ts index 4ba7ec562..e63c1fa89 100644 --- a/src/routes/admin/profiles/common.ts +++ b/src/routes/admin/profiles/common.ts @@ -14,7 +14,8 @@ import { NodeForge } from '../../../NodeForge.js' import { type ISecretManagerService } from '../../../interfaces/ISecretManagerService.js' import { CertManager } from '../../../certManager.js' import Logger from '../../../Logger.js' -import { ClientAction, TlsSigningAuthority } from '../../../models/RCS.Config.js' +import { ClientAction, TlsMode, TlsSigningAuthority } from '../../../models/RCS.Config.js' +import { stringify } from 'querystring' export function adjustTlsConfiguration(amtConfig: AMTConfiguration): AMTConfiguration { // default to self-signed if tls is indicated @@ -27,7 +28,9 @@ export function adjustTlsConfiguration(amtConfig: AMTConfiguration): AMTConfigur // clear out authority if it shouldn't be there. if (!amtConfig.tlsMode && amtConfig.tlsSigningAuthority) { amtConfig.tlsSigningAuthority = null + amtConfig.tlsSigningAuthorityURL = null } + return amtConfig } diff --git a/src/routes/admin/profiles/create.test.ts b/src/routes/admin/profiles/create.test.ts index a94c989e3..8fdd0f1d5 100644 --- a/src/routes/admin/profiles/create.test.ts +++ b/src/routes/admin/profiles/create.test.ts @@ -11,6 +11,7 @@ import { AMTUserConsent } from '../../../models/index.js' import { adjustRedirectionConfiguration } from './common.js' import { jest } from '@jest/globals' import { type SpyInstance, spyOn } from 'jest-mock' +import { tls } from 'node-forge' describe('Profiles - Create', () => { let resSpy @@ -182,6 +183,23 @@ describe('Profiles - Create', () => { ...defaultRedirectionCfgACM }) expect(writeSecretSpy).not.toHaveBeenNthCalledWith(2, `TLS/${req.body.profileName}`, expect.anything()) + }) + it('should handle error if TLS certificate authority URL is empty', async () => { + req.body = { + ...sparseAcmCfg, + tlsMode: TlsMode.SERVER_ALLOW_NONTLS, + tlsSigningAuthority: TlsSigningAuthority.CUSTOM_CA, + verifyTlsSigningAuthorityURL: null + } + await createProfile(req, resSpy) + expect(insertSpy).toHaveBeenCalledWith({ + ...req.body, + amtPassword: 'AMT_PASSWORD', + mebxPassword: 'MEBX_PASSWORD', + + ...defaultRedirectionCfgACM + }) + expect(resSpy.status).toHaveBeenCalledWith(500) }) it(`should set default AMT Redirection Configuration settings for ${ClientAction.ADMINCTLMODE}`, async () => { req.body = { @@ -204,7 +222,7 @@ describe('Profiles - Create', () => { userConsent: AMTUserConsent.ALL }) }) - it('should handle error', async () => { + it('should handle error if insert fails', async () => { spyOn(req.db.profiles, 'insert').mockResolvedValue(null) await createProfile(req, resSpy) expect(insertSpy).toHaveBeenCalledWith({ diff --git a/src/routes/admin/profiles/create.ts b/src/routes/admin/profiles/create.ts index 2ad5d3285..411653c17 100644 --- a/src/routes/admin/profiles/create.ts +++ b/src/routes/admin/profiles/create.ts @@ -64,8 +64,12 @@ export async function createProfile(req: Request, res: Response): Promise throw new Error('Error saving password to secret provider. AMT Profile not inserted') } } - // generate self-signed certificates for use with TLS config if applicable + // generate self-signed certificates for use with TLS config if applicable or validate CUSTOM_CA has a TLS Authority URL if (amtConfig.tlsMode != null) { + if (amtConfig.tlsSigningAuthority === TlsSigningAuthority.CUSTOM_CA && !amtConfig.tlsSigningAuthorityURL) { + throw new Error('Error during TLS signing authority validation. You must provide a valid signing authority URL') + } + // API compatibility: default to self-signed if no other option is indicated if (!amtConfig.tlsSigningAuthority || amtConfig.tlsSigningAuthority === TlsSigningAuthority.SELF_SIGNED) { await generateSelfSignedCertificate(req.secretsManager, amtConfig.profileName) diff --git a/src/routes/admin/profiles/edit.ts b/src/routes/admin/profiles/edit.ts index fb81bf18a..19e4d9776 100644 --- a/src/routes/admin/profiles/edit.ts +++ b/src/routes/admin/profiles/edit.ts @@ -184,6 +184,7 @@ export const getUpdatedData = async (newConfig: any, oldConfig: AMTConfiguration amtConfig.kvmEnabled = newConfig.kvmEnabled ?? oldConfig.kvmEnabled amtConfig.solEnabled = newConfig.solEnabled ?? oldConfig.solEnabled amtConfig.tlsSigningAuthority = newConfig.tlsSigningAuthority ?? oldConfig.tlsSigningAuthority + amtConfig.tlsSigningAuthorityURL = newConfig.tlsSigningAuthorityURL ?? oldConfig.tlsSigningAuthorityURL amtConfig.ieee8021xProfileName = newConfig.ieee8021xProfileName amtConfig.version = newConfig.version return amtConfig