Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add credHelpers to produce credentials #1244

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plenty-knives-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sigstore/oci': minor
---

getRegistryCredentials can now use credHelpers from docker configuration file
4 changes: 2 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const compat = new FlatCompat({
});

export default [{
ignores: ["**/node_modules", "**/dist", "**/__generated__", "**/__fixtures__", "**/jest.config.js", "**/jest.config.base.js"],
ignores: ["**/node_modules", "**/dist", "**/__generated__", "**/__fixtures__", "**/jest.config.js", "**/jest.config.base.js", "**/hack"],
}, ...compat.extends(
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
Expand All @@ -32,4 +32,4 @@ export default [{
"@typescript-eslint/no-unused-vars": ["error", { "caughtErrors": "none" }],
"@typescript-eslint/no-require-imports": "off"
},
}];
}];
93 changes: 93 additions & 0 deletions packages/oci/src/__tests__/credentials.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ limitations under the License.
*/
import fs from 'fs';
import os from 'os';
import child_process, { ExecFileSyncOptions } from 'node:child_process';
import path from 'path';
import {
fromBasicAuth,
Expand All @@ -25,18 +26,38 @@ import {
describe('getRegistryCredentials', () => {
const registryName = 'my-registry';
const imageName = `${registryName}/my-image`;
const badRegistryName = 'bad-registry';
const imageNameBadRegistry = `${badRegistryName}/my-image`;
let homedirSpy: jest.SpyInstance<string, []> | undefined;
let execSpy: jest.SpyInstance<string | Buffer, [file: string, args?: readonly string[] | undefined, options?: ExecFileSyncOptions | undefined]> | undefined;
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'get-reg-creds-'));
const dockerDir = path.join(tempDir, '.docker');
fs.mkdirSync(dockerDir, { recursive: true });

beforeEach(() => {
homedirSpy = jest.spyOn(os, 'homedir');
homedirSpy.mockReturnValue(tempDir);

execSpy = jest.spyOn(child_process, 'execFileSync');
execSpy?.mockImplementation((file, args, options) => {
if (file!=="docker-credential-fake" || args?.length!=1 || args[0]!=="get"){
throw "Invalid arguments";
}

if (options?.input === `${registryName}`) {
const credentials = {
Username: 'username',
Secret: 'password'
};
return JSON.stringify(credentials);
}
throw "Invalid registry";
});
});

afterEach(() => {
homedirSpy?.mockRestore();
execSpy?.mockRestore();
});

afterAll(() => {
Expand Down Expand Up @@ -168,8 +189,80 @@ describe('getRegistryCredentials', () => {
expect(creds).toEqual({ username, password });
});
});

describe('when credHelper exit in error', () => {
const dockerConfig = {
credHelpers: {
[badRegistryName]: "fake"
}
};

beforeEach(() => {
fs.writeFileSync(
path.join(tempDir, '.docker', 'config.json'),
JSON.stringify(dockerConfig),
{}
);
});

it('throws an error', () => {
expect(() => getRegistryCredentials(imageNameBadRegistry)).toThrow(
/Failed to get credentials from helper fake for registry bad-registry/i
);
});
});


describe('when credHelper exist for the registry', () => {
const username = 'username';
const password = 'password';

const dockerConfig = {
credHelpers: {
[registryName]: "fake"
}
};

beforeEach(() => {
fs.writeFileSync(
path.join(tempDir, '.docker', 'config.json'),
JSON.stringify(dockerConfig),
{}
);
});

it('returns the credentials', () => {
const creds = getRegistryCredentials(imageName);

expect(creds).toEqual({ username, password });
});
});

describe('when credsStore exist', () => {
const username = 'username';
const password = 'password';

const dockerConfig = {
credsStore: "fake"
};

beforeEach(() => {
fs.writeFileSync(
path.join(tempDir, '.docker', 'config.json'),
JSON.stringify(dockerConfig),
{}
);
});

it('returns the credentials', () => {
const creds = getRegistryCredentials(imageName);

expect(creds).toEqual({ username, password });
});
});
});


describe('toBasicAuth', () => {
const creds = { username: 'user', password: 'pass' };
const expected = 'dXNlcjpwYXNz';
Expand Down
90 changes: 74 additions & 16 deletions packages/oci/src/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ limitations under the License.
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
import { parseImageName } from './name';

export type Credentials = {
Expand All @@ -24,32 +25,21 @@ export type Credentials = {
};

type DockerConifg = {
auths?: { [registry: string]: { auth: string; identitytoken?: string } };
credsStore?: string,
auths?: { [registry: string]: { auth: string; identitytoken?: string } },
credHelpers?: { [registry: string]: string },
};

// Returns the credentials for a given registry by reading the Docker config
// file.
export const getRegistryCredentials = (imageName: string): Credentials => {
const { registry } = parseImageName(imageName);
const dockerConfigFile = path.join(os.homedir(), '.docker', 'config.json');

let content: string | undefined;
try {
content = fs.readFileSync(dockerConfigFile, 'utf8');
} catch (err) {
throw new Error(`No credential file found at ${dockerConfigFile}`);
}

const dockerConfig: DockerConifg = JSON.parse(content);

function credentialFromAuths(dockerConfig: DockerConifg, registry: string) {
const credKey =
Object.keys(dockerConfig?.auths || {}).find((key) =>
key.includes(registry)
) || registry;
const creds = dockerConfig?.auths?.[credKey];

if (!creds) {
throw new Error(`No credentials found for registry ${registry}`);
return null;
}

// Extract username/password from auth string
Expand All @@ -59,6 +49,74 @@ export const getRegistryCredentials = (imageName: string): Credentials => {
const pass = creds.identitytoken ? creds.identitytoken : password;

return { username, password: pass };
}

function credentialFromCredHelpers(dockerConfig: DockerConifg, registry: string) {
// Check if the registry has a credHelper and use it if it does
const helper = dockerConfig?.credHelpers?.[registry];

if (!helper) {
return null;
}

return launchHelper(helper, registry);
}

function credentialFromCredsStore(dockerConfig: DockerConifg, registry: string) {
// If the credsStore is set, use it to get the credentials
const helper = dockerConfig?.credsStore;

if (!helper) {
return null;
}

return launchHelper(helper, registry);
}

function launchHelper(helper: string, registry: string) {
// Get the credentials from the helper.
// Parameter for helper is 'get' and registry is passed as input
// The helper should return a JSON object with the keys "Username" and "Secret"
try {
const output = execFileSync(`docker-credential-${helper}`, ["get"], {
input: registry,
}).toString();

const { Username: username, Secret: password } = JSON.parse(output);

return { username, password };
} catch (err) {
throw new Error(`Failed to get credentials from helper ${helper} for registry ${registry}: ${err}`);
}
}

// file.
export const getRegistryCredentials = (imageName: string): Credentials => {
const { registry } = parseImageName(imageName);
const dockerConfigFile = path.join(os.homedir(), '.docker', 'config.json');

let content: string | undefined;
try {
content = fs.readFileSync(dockerConfigFile, 'utf8');
} catch (err) {
throw new Error(`No credential file found at ${dockerConfigFile}`);
}

const dockerConfig: DockerConifg = JSON.parse(content);

const fromAuths=credentialFromAuths(dockerConfig, registry);
if (fromAuths) {
return fromAuths;
}
const fromCredHelpers=credentialFromCredHelpers(dockerConfig, registry);
if (fromCredHelpers) {
return fromCredHelpers;
}
const fromCredsStore=credentialFromCredsStore(dockerConfig, registry);
if (fromCredsStore) {
return fromCredsStore;
}
throw new Error(`No credentials found for registry ${registry}`);
};

// Encode the username and password as base64-encoded basicauth value
Expand Down