fleet/website/api/controllers/get-est-device-certificate.js

153 lines
4.3 KiB
JavaScript
Vendored

module.exports = {
friendlyName: 'Get a device certificate via EST protocol',
description: 'Take a certificate signing request and authentication token, then use the EST protocol to request a certificate to be issued.',
extendedDescription: 'This action is the result of a customer hackathon working on issuing device certificates to Linux devices.',
moreInfoUrl: 'https://github.com/fleetdm/confidential/issues/8785',
inputs: {
csrData: {
required: true,
type: 'string',
description: 'Certificate Signing Request (CSR) data'
},
authToken: {
required: true,
type: 'string',
description: 'Authorization token provided by IdP',
},
introspectEndpoint: {
required: true,
type: 'string',
description: 'IdP introspect endpoint URL'
},
idpClientId: {
required: true,
type: 'string',
description: 'IdP client ID'
},
estEndpoint: {
required: true,
type: 'string',
description: 'EST protocol endpoint URL'
},
estClientId: {
required: true,
type: 'string',
description: 'EST client ID'
},
estClientKey: {
required: true,
type: 'string',
description: 'EST client key'
}
},
exits: {
success: {
description: 'Successfully generated certificate',
extendedDescription: 'This action is the result of a hackathon where we were relying on this datashape.',
outputType: {certificate:'string'},
outputFriendlyName: 'Certificate',
},
invalidToken: {
description: 'The IdP auth token is invalid',
statusCode: 403,
},
invalidCsr: {
description: 'The provided CSR data was invalid.',
responseType: 'badRequest'
}
},
fn: async function ({ csrData, authToken, introspectEndpoint, idpClientId, estEndpoint, estClientId, estClientKey }) {
// Ask the IdP to introspect the auth token (ensure it's valid and extract the values).
const introspectResponse = await sails.helpers.http.sendHttpRequest.with({
url: introspectEndpoint,
method: 'POST',
enctype: 'application/x-www-form-urlencoded',
body: {
'client_id': idpClientId,
'token': authToken,
},
});
if (!introspectResponse.body) {
throw 'invalidToken';
}
const introspectBody = JSON.parse(introspectResponse.body);
if (!introspectBody.active) {
throw 'invalidToken';
}
const introspectUsername = introspectBody.username;
// Extract the email and username from the CSR. Ensure they match.
let jsrsasign = require('jsrsasign');
const csrUtil = jsrsasign.asn1.csr.CSRUtil;
const csrObj = csrUtil.getParam(csrData);
let csrEmail = '';
let csrUsername = '';
for (const extension of csrObj.extreq) {
if (extension.extname === 'subjectAltName') {
for (const extentry of extension.array) {
if ('rfc822' in extentry) {
csrEmail = extentry.rfc822;
}
if ('other' in extentry) {
if ('oid' in extentry.other && extentry.other.oid === '1.3.6.1.4.1.311.20.2.3') {
csrUsername = extentry.other.value.utf8str.str;
}
}
}
}
}
if (csrEmail === '') {
throw 'invalidCsr';
}
if (!csrEmail.startsWith(csrUsername)) {
throw 'invalidCsr';
}
// Ensure username from IdP auth matches username in CSR. If they don't match, perhaps the user
// is trying to get a certificate with another user's name?
if (csrEmail !== introspectUsername) {
throw 'invalidToken';
}
// Ask the PKI provider for a certificate
const request = require('@sailshq/request');
const estResponse = await new Promise((resolve, reject) => {
request({
url: estEndpoint,
method: 'POST',
body: csrData.replace(/(-----(BEGIN|END) CERTIFICATE REQUEST-----|\n)/g, ''),
headers: {
'Content-Type': 'application/pkcs10',
'Authorization': `Basic ${Buffer.from(`${estClientId}:${estClientKey}`).toString('base64')}`,
},
}, (err, response)=>{
if (err) {
reject(err);
} else {
response.body = '-----BEGIN CERTIFICATE-----\n' + response.body + '\n-----END CERTIFICATE-----';
resolve(response);
}
});
});
return estResponse.body;
}
};