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

Adds support for new Rekor 'dsse' entry type #527

Merged
merged 7 commits into from
Jun 5, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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/little-houses-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sigstore/rekor-types': minor
---

add DSSE type
76 changes: 76 additions & 0 deletions packages/client/src/__tests__/tlog/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {
toProposedDSSEEntry,
toProposedHashedRekordEntry,
toProposedIntotoEntry,
} from '../../tlog/format';
Expand Down Expand Up @@ -55,6 +56,81 @@ describe('format', () => {
});
});

describe('toProposedDSSEEntry', () => {
describe('when there is a single signature in the envelope', () => {
const envelope: Envelope = {
payloadType: 'application/vnd.in-toto+json',
payload: Buffer.from('payload'),
signatures: [{ keyid: '123', sig: signature }],
};

it('returns a valid dsse entry', () => {
const entry = toProposedDSSEEntry(envelope, sigMaterial);

expect(entry.apiVersion).toEqual('0.0.1');
expect(entry.kind).toEqual('dsse');
expect(entry.spec).toBeTruthy();
expect(entry.spec.proposedContent).toBeTruthy();
expect(typeof entry.spec.proposedContent?.envelope).toBe('string');
expect(entry.spec.proposedContent?.verifiers).toHaveLength(1);
expect(entry.spec.proposedContent?.verifiers[0]).toEqual(
enc.base64Encode(cert)
);

// ensure we have the expected JSON object stored in the string
if (typeof entry.spec.proposedContent?.envelope === 'string') {
const envObj = JSON.parse(entry.spec.proposedContent?.envelope);
expect(envObj).toEqual(Envelope.toJSON(envelope));
// ensure we only have 1 signature specified in the object
expect(envObj.signatures).toHaveLength(1);
} else {
fail('dsse envelope should be set as JSON string');
}

// we don't want the persisted properties to show up in a proposed entry
expect(entry.spec.signatures).toBeUndefined();
});
});

describe('when there are multiple signatures in the envelope', () => {
const envelope: Envelope = {
payloadType: 'application/vnd.in-toto+json',
payload: Buffer.from('payload'),
signatures: [
{ keyid: '123', sig: signature },
{ keyid: '456', sig: signature },
],
};

it('returns a valid dsse entry', () => {
const entry = toProposedDSSEEntry(envelope, sigMaterial);

expect(entry.apiVersion).toEqual('0.0.1');
expect(entry.kind).toEqual('dsse');
expect(entry.spec).toBeTruthy();
expect(entry.spec.proposedContent).toBeTruthy();
expect(typeof entry.spec.proposedContent?.envelope).toBe('string');
expect(entry.spec.proposedContent?.verifiers).toHaveLength(1);
expect(entry.spec.proposedContent?.verifiers[0]).toEqual(
enc.base64Encode(cert)
);

// ensure we have the expected JSON object stored in the string
if (typeof entry.spec.proposedContent?.envelope === 'string') {
const envObj = JSON.parse(entry.spec.proposedContent?.envelope);
expect(envObj).toEqual(Envelope.toJSON(envelope));
// ensure we have 2 signatures specified in the object
expect(envObj.signatures).toHaveLength(2);
} else {
fail('dsse envelope should be set as JSON string');
}

// we don't want the persisted properties to show up in a proposed entry
expect(entry.spec.signatures).toBeUndefined();
});
});
});

describe('toProposedIntotoEntry', () => {
describe('when the keyid is a non-empty string', () => {
const envelope: Envelope = {
Expand Down
2 changes: 2 additions & 0 deletions packages/client/src/external/rekor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { checkStatus } from './error';
import type {
LogEntry,
ProposedEntry,
ProposedDSSEEntry,
ProposedHashedRekordEntry,
ProposedIntotoEntry,
SearchIndex,
Expand All @@ -31,6 +32,7 @@ export type {
ProposedEntry,
SearchIndex,
SearchLogQuery,
ProposedDSSEEntry,
ProposedHashedRekordEntry,
ProposedIntotoEntry,
};
Expand Down
44 changes: 38 additions & 6 deletions packages/client/src/tlog/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,30 @@ import { Envelope } from '../types/sigstore';
import { crypto, encoding as enc, json } from '../util';

import type {
ProposedDSSEEntry,
ProposedHashedRekordEntry,
ProposedIntotoEntry,
} from '../external/rekor';

const DEFAULT_DSSE_API_VERSION = '0.0.1';
const DEFAULT_HASHEDREKORD_API_VERSION = '0.0.1';
const DEFAULT_INTOTO_API_VERSION = '0.0.2';

// Returns a properly formatted Rekor "dsse" entry for the given DSSE
// envelope and signature
export function toProposedDSSEEntry(
envelope: Envelope,
signature: SignatureMaterial,
apiVersion = DEFAULT_DSSE_API_VERSION
): ProposedDSSEEntry {
switch (apiVersion) {
case '0.0.1':
return toProposedDSSEV001Entry(envelope, signature);
default:
throw new Error(`Unsupported dsse kind API version: ${apiVersion}`);
}
}

// Returns a properly formatted Rekor "hashedrekord" entry for the given digest
// and signature
export function toProposedHashedRekordEntry(
Expand Down Expand Up @@ -69,6 +86,21 @@ export function toProposedIntotoEntry(
throw new Error(`Unsupported intoto kind API version: ${apiVersion}`);
}
}
function toProposedDSSEV001Entry(
envelope: Envelope,
signature: SignatureMaterial
): ProposedDSSEEntry {
return {
apiVersion: '0.0.1',
kind: 'dsse',
spec: {
proposedContent: {
envelope: JSON.stringify(Envelope.toJSON(envelope)),
verifiers: [enc.base64Encode(toPublicKey(signature))],
},
},
};
}

function toProposedIntotoV002Entry(
envelope: Envelope,
Expand All @@ -90,7 +122,7 @@ function toProposedIntotoV002Entry(
// Create the envelope portion of the entry. Note the inclusion of the
// publicKey in the signature struct is not a standard part of a DSSE
// envelope, but is required by Rekor.
const dsse: ProposedIntotoEntry['spec']['content']['envelope'] = {
const dsseEnv: ProposedIntotoEntry['spec']['content']['envelope'] = {
payloadType: envelope.payloadType,
payload: payload,
signatures: [{ sig, publicKey }],
Expand All @@ -100,15 +132,15 @@ function toProposedIntotoV002Entry(
// need to do the same here so that we can properly recreate the entry for
// verification.
if (keyid.length > 0) {
dsse.signatures[0].keyid = keyid;
dsseEnv.signatures[0].keyid = keyid;
}

return {
apiVersion: '0.0.2',
kind: 'intoto',
spec: {
content: {
envelope: dsse,
envelope: dsseEnv,
hash: { algorithm: 'sha256', value: envelopeHash },
payloadHash: { algorithm: 'sha256', value: payloadHash },
},
Expand All @@ -127,7 +159,7 @@ function calculateDSSEHash(
envelope: Envelope,
signature: SignatureMaterial
): string {
const dsse: ProposedIntotoEntry['spec']['content']['envelope'] = {
const dsseEnv: ProposedIntotoEntry['spec']['content']['envelope'] = {
payloadType: envelope.payloadType,
payload: envelope.payload.toString('base64'),
signatures: [
Expand All @@ -140,10 +172,10 @@ function calculateDSSEHash(

// If the keyid is an empty string, Rekor seems to remove it altogether.
if (envelope.signatures[0].keyid.length > 0) {
dsse.signatures[0].keyid = envelope.signatures[0].keyid;
dsseEnv.signatures[0].keyid = envelope.signatures[0].keyid;
}

return crypto.hash(json.canonicalize(dsse)).toString('hex');
return crypto.hash(json.canonicalize(dsseEnv)).toString('hex');
}

function toPublicKey(signature: SignatureMaterial): string {
Expand Down
64 changes: 64 additions & 0 deletions packages/client/src/tlog/verify/body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { crypto, encoding as enc } from '../../util';

import type {
ProposedEntry,
ProposedDSSEEntry,
ProposedHashedRekordEntry,
ProposedIntotoEntry,
} from '../../external/rekor';
Expand All @@ -41,6 +42,9 @@ export function verifyTLogBody(
}

switch (body.kind) {
case 'dsse':
verifyDSSETLogBody(body, bundleContent);
break;
case 'intoto':
verifyIntotoTLogBody(body, bundleContent);
break;
Expand All @@ -56,6 +60,30 @@ export function verifyTLogBody(
}
}

// Compare the given intoto tlog entry to the given bundle
function verifyDSSETLogBody(
tlogEntry: ProposedDSSEEntry,
content: sigstore.Bundle['content']
): void {
if (content?.$case !== 'dsseEnvelope') {
throw new VerificationError(
`unsupported bundle content: ${content?.$case || 'unknown'}`
);
}

const dsse = content.dsseEnvelope;

switch (tlogEntry.apiVersion) {
case '0.0.1':
verifyDSSE001TLogBody(tlogEntry, dsse);
break;
default:
throw new VerificationError(
`unsupported dsse version: ${tlogEntry.apiVersion}`
);
}
}

// Compare the given intoto tlog entry to the given bundle
function verifyIntotoTLogBody(
tlogEntry: ProposedIntotoEntry,
Expand Down Expand Up @@ -104,6 +132,42 @@ function verifyHashedRekordTLogBody(
}
}

// Compare the given dsse v0.0.1 tlog entry to the given DSSE envelope.
function verifyDSSE001TLogBody(
tlogEntry: Extract<ProposedDSSEEntry, { apiVersion: '0.0.1' }>,
dsse: sigstore.Envelope
): void {
// Collect all of the signatures from the DSSE envelope
// Turns them into base64-encoded strings for comparison
const dsseSigs = dsse.signatures.map((signature) =>
signature.sig.toString('base64')
);

// Collect all of the signatures from the tlog entry
// Remember that tlog signatures are double base64-encoded
const tlogSigs = tlogEntry.spec.signatures?.map((signature) =>
signature.signature ? enc.base64Decode(signature.signature) : ''
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment about the double base64 encoding still true for the DSSE type?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a test in staging and I don't think we need to decode the signature at all here. The sig value from the DSSE envelope above is b64 encoded which should then match the value we pull from the tlog entry here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you have docs somewhere on how to target staging with the client? I had to make several code changes when I was doing that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a new CLI in the "packages/cli" directory which supports a flag for overriding the rekor URL. From the root of the project you can do:

./packages/cli/bin/dev attest --help

That should show you the various flags


// Ensure the bundle's DSSE and the tlog entry contain the same number of signatures
if (dsseSigs.length !== tlogSigs?.length) {
throw new VerificationError(TLOG_MISMATCH_ERROR_MSG);
}

// Ensure that every signature in the bundle's DSSE is present in the tlog entry
if (!dsseSigs.every((dsseSig) => tlogSigs.includes(dsseSig))) {
throw new VerificationError(TLOG_MISMATCH_ERROR_MSG);
}

// Ensure the digest of the bundle's DSSE payload matches the digest in the
// tlog entry
const dssePayloadHash = crypto.hash(dsse.payload).toString('hex');

if (dssePayloadHash !== tlogEntry.spec.payloadHash?.value) {
throw new VerificationError(TLOG_MISMATCH_ERROR_MSG);
}
}

// Compare the given intoto v0.0.2 tlog entry to the given DSSE envelope.
function verifyIntoto002TLogBody(
tlogEntry: Extract<ProposedIntotoEntry, { apiVersion: '0.0.2' }>,
Expand Down
2 changes: 1 addition & 1 deletion packages/rekor-types/hack/generate-rekor-types
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ npx openapi --input "${REKOR_DIR}/openapi.yaml" \
--exportSchemas=false

# Run json2ts on schemas
KINDS=( intoto hashedrekord )
KINDS=( dsse intoto hashedrekord )
for KIND in "${KINDS[@]}"
do
TYPE_PATH=${REKOR_DIR}/pkg/types/${KIND}
Expand Down
Loading