diff --git a/README.md b/README.md index 15f4d6f..5041282 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,8 @@ with: githubToken: ${{ secrets.MY_GITHUB_TOKEN }} caCertificate: ${{ secrets.VAULTCA }} ``` -- **jwt**: you must provide a `role` & `jwtPrivateKey` parameters, additionally you can pass `jwtKeyPassword` & `jwtTtl` parameters +- **jwt**: you must provide a `role` parameter, additionally you can pass `jwtPrivateKey`, `jwtKeyPassword` & `jwtTtl` parameters. + Github provided JWT will be used if `jwtPrivateKey` was not specified ```yaml ... with: @@ -98,6 +99,15 @@ with: jwtTtl: 3600 # 1 hour, default value ``` +**Notice:** In order for Github provided JWT to work workflow should have `id-token: write` specified in the `permissions` section of a workflow + +```yaml +... +permissions: + id-token: write +... +``` + - **kubernetes**: you must provide the `role` paramaters. You can optionally override the `kubernetesTokenPath` paramater for custom mounted serviceAccounts. Consider [kubernetes auth](https://www.vaultproject.io/docs/auth/kubernetes) when using self-hosted runners on Kubernetes: ```yaml ... diff --git a/dist/index.js b/dist/index.js index fc462b9..4b92ff2 100644 --- a/dist/index.js +++ b/dist/index.js @@ -977,6 +977,7 @@ exports.default = parseBody; const core = __webpack_require__(470); const rsasign = __webpack_require__(758); const fs = __webpack_require__(747); +const got = __webpack_require__(77).default; const defaultKubernetesTokenPath = '/var/run/secrets/kubernetes.io/serviceaccount/token' /*** @@ -999,11 +1000,21 @@ async function retrieveToken(method, client) { } case 'jwt': { const role = core.getInput('role', { required: true }); - const privateKeyRaw = core.getInput('jwtPrivateKey', { required: true }); + const privateKeyRaw = core.getInput('jwtPrivateKey', { required: false }); const privateKey = Buffer.from(privateKeyRaw, 'base64').toString(); const keyPassword = core.getInput('jwtKeyPassword', { required: false }); const tokenTtl = core.getInput('jwtTtl', { required: false }) || '3600'; // 1 hour - const jwt = generateJwt(privateKey, keyPassword, Number(tokenTtl)); + /** @type {string} */ + let jwt; + const actionsIDTokenRequestToken = process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN']; + const actionsIDTokenRequestURL = process.env['ACTIONS_ID_TOKEN_REQUEST_URL']; + + if (!privateKeyRaw && actionsIDTokenRequestToken && actionsIDTokenRequestURL) { + jwt = await getJwt(actionsIDTokenRequestToken, `${actionsIDTokenRequestURL}&audience=sigstore`); + } else { + jwt = generateJwt(privateKey, keyPassword, Number(tokenTtl)); + } + return await getClientToken(client, method, path, { jwt: jwt, role: role }); } case 'kubernetes': { @@ -1057,6 +1068,34 @@ function generateJwt(privateKey, keyPassword, ttl) { return rsasign.KJUR.jws.JWS.sign(alg, JSON.stringify(header), JSON.stringify(payload), decryptedKey); } +/*** + * Call the appropriate endpoint and retrieves job's JWT + * @param {string} actionsIDTokenRequestToken + * @param {string} actionsIDTokenRequestURL + */ +async function getJwt(actionsIDTokenRequestToken, actionsIDTokenRequestURL) { + /** @type {'json'} */ + const responseType = 'json'; + const options = { + headers: { + Authorization: `Bearer ${actionsIDTokenRequestToken}`, + }, + responseType, + }; + const client = got.extend(options) + + core.debug(`Retrieving Vault JWT from ${actionsIDTokenRequestURL} endpoint`); + /** @type {import('got').Response} */ + const response = await client.get(actionsIDTokenRequestURL, options); + + if (response && response.body && response.body.value) { + core.debug('✔ Github Actions ID Token successfully retrieved'); + return response.body.value; + } else { + throw Error(`Unable to retrieve token from ${actionsIDTokenRequestURL}'s endpoint.`); + } +} + /*** * Call the appropriate login endpoint and parse out the token in the response. * @param {import('got').Got} client @@ -1102,6 +1141,12 @@ async function getClientToken(client, method, path, payload) { * }} auth */ +/*** + * @typedef {Object} GithubActionsIdTokenResponse + * @property {string} value + * @property {string} count + */ + module.exports = { retrieveToken, }; diff --git a/package-lock.json b/package-lock.json index 3f2d8e3..f5576fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13872,7 +13872,7 @@ "hook-std": "^2.0.0", "hosted-git-info": "^3.0.0", "lodash": "^4.17.15", - "marked": "^2.0.0", + "marked": "^1.0.0", "marked-terminal": "^4.0.0", "micromatch": "^4.0.2", "p-each-series": "^2.1.0", diff --git a/src/auth.js b/src/auth.js index 4b05b77..e893a2c 100644 --- a/src/auth.js +++ b/src/auth.js @@ -2,6 +2,7 @@ const core = require('@actions/core'); const rsasign = require('jsrsasign'); const fs = require('fs'); +const got = require('got').default; const defaultKubernetesTokenPath = '/var/run/secrets/kubernetes.io/serviceaccount/token' /*** @@ -24,11 +25,21 @@ async function retrieveToken(method, client) { } case 'jwt': { const role = core.getInput('role', { required: true }); - const privateKeyRaw = core.getInput('jwtPrivateKey', { required: true }); + const privateKeyRaw = core.getInput('jwtPrivateKey', { required: false }); const privateKey = Buffer.from(privateKeyRaw, 'base64').toString(); const keyPassword = core.getInput('jwtKeyPassword', { required: false }); const tokenTtl = core.getInput('jwtTtl', { required: false }) || '3600'; // 1 hour - const jwt = generateJwt(privateKey, keyPassword, Number(tokenTtl)); + /** @type {string} */ + let jwt; + const actionsIDTokenRequestToken = process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN']; + const actionsIDTokenRequestURL = process.env['ACTIONS_ID_TOKEN_REQUEST_URL']; + + if (!privateKeyRaw && actionsIDTokenRequestToken && actionsIDTokenRequestURL) { + jwt = await getJwt(actionsIDTokenRequestToken, `${actionsIDTokenRequestURL}&audience=sigstore`); + } else { + jwt = generateJwt(privateKey, keyPassword, Number(tokenTtl)); + } + return await getClientToken(client, method, path, { jwt: jwt, role: role }); } case 'kubernetes': { @@ -82,6 +93,34 @@ function generateJwt(privateKey, keyPassword, ttl) { return rsasign.KJUR.jws.JWS.sign(alg, JSON.stringify(header), JSON.stringify(payload), decryptedKey); } +/*** + * Call the appropriate endpoint and retrieves job's JWT + * @param {string} actionsIDTokenRequestToken + * @param {string} actionsIDTokenRequestURL + */ +async function getJwt(actionsIDTokenRequestToken, actionsIDTokenRequestURL) { + /** @type {'json'} */ + const responseType = 'json'; + const options = { + headers: { + Authorization: `Bearer ${actionsIDTokenRequestToken}`, + }, + responseType, + }; + const client = got.extend(options) + + core.debug(`Retrieving Vault JWT from ${actionsIDTokenRequestURL} endpoint`); + /** @type {import('got').Response} */ + const response = await client.get(actionsIDTokenRequestURL, options); + + if (response && response.body && response.body.value) { + core.debug('✔ Github Actions ID Token successfully retrieved'); + return response.body.value; + } else { + throw Error(`Unable to retrieve token from ${actionsIDTokenRequestURL}'s endpoint.`); + } +} + /*** * Call the appropriate login endpoint and parse out the token in the response. * @param {import('got').Got} client @@ -127,6 +166,12 @@ async function getClientToken(client, method, path, payload) { * }} auth */ +/*** + * @typedef {Object} GithubActionsIdTokenResponse + * @property {string} value + * @property {string} count + */ + module.exports = { retrieveToken, };