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 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/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
44 changes: 44 additions & 0 deletions packages/client/src/__tests__/__fixtures__/bundles/dsse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,49 @@ const validBundleWithNoTLogEntries = {
},
};

const validBundleWithDSSETLogEntry = {
mediaType: 'application/vnd.dev.sigstore.bundle+json;version=0.1',
verificationMaterial: {
x509CertificateChain: {
certificates: [
{
rawBytes:
'MIIC0TCCAlagAwIBAgIUOBERpxhVuZTd8XrNJheYDQ9iszAwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjMwNjA1MTc1MjU0WhcNMjMwNjA1MTgwMjU0WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExoIQxR/RxXl20dAM7pcKlC5fEwRexmHMCzsXaPpUpror/AQlst/WGcvHkbtGXciBWVkrnxnHo1I5O+73FxBrRaOCAXUwggFxMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUBhcYgqIg8VwrdSF1kVTOtPPFNSkwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0RAQH/BBUwE4ERYnJpYW5AZGVoYW1lci5jb20wLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMC4GCisGAQQBg78wAQgEIAweaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGIjLDD8QAABAMARzBFAiAU40oVa45EOf1Miq1b+PEFFG8a/51N/ovrHcy74w9elgIhAJ0JMviR309CWzosD9k/iq9s+Ij3xjNdru4gmsf+ejV2MAoGCCqGSM49BAMDA2kAMGYCMQDK7j57m6kiLX6b/0akP4SLewDSKrpf7Nx+PdbeAe95TyQGtZJC+Z+3jTIp9zosxpECMQDqPIQLDyokUecisWPVJ2GUxsT0yw1hp3LYu63sdGxUSV68ms7QvWUKkfaBRGq+sNk=',
},
],
},
tlogEntries: [
{
logIndex: '22797666',
logId: {
keyId: 'wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=',
},
kindVersion: {
kind: 'dsse',
version: '0.0.1',
},
integratedTime: '1685987575',
inclusionPromise: {
signedEntryTimestamp:
'MEQCIHhAfEwtfkiRK/XD1EbjArDzf9svz75oqUqfT6Mha6PHAiBE+ibnkU7x/AgKMshVT23+DLc3+OtTozW5eulQKrBQnw==',
},
canonicalizedBody:
'eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZjRjMTc1ZTVlZjQ2YjEyMzc4NzQ3MDZkYjFhYzE1YmY5ZGYzYTg5MGJlNmY1MmEyNzY0Y2QyZGFiMzJjZWQwNyJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjY4ZTY1NmIyNTFlNjdlODM1OGJlZjg0ODNhYjBkNTFjNjYxOWYzZTdhMWE5ZjBlNzU4MzhkNDFmZjM2OGY3MjgifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVZQ0lRRHFLRHQ2MTk3dnFjYkM3Rys0YkF6NWkrYitnSUVIRjdiMG1uWkJJejZvMmdJaEFPNG84WFdBZFdZRGUwRjZOTHVpK3hLSVA3a2hvQVUzNDZnSmYwUzNxVFpvIiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VNd1ZFTkRRV3hoWjBGM1NVSkJaMGxWVDBKRlVuQjRhRloxV2xSa09GaHlUa3BvWlZsRVVUbHBjM3BCZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwTmQwNXFRVEZOVkdNeFRXcFZNRmRvWTA1TmFrMTNUbXBCTVUxVVozZE5hbFV3VjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVjRiMGxSZUZJdlVuaFliREl3WkVGTk4zQmpTMnhETldaRmQxSmxlRzFJVFVONmMxZ0tZVkJ3VlhCeWIzSXZRVkZzYzNRdlYwZGpka2hyWW5SSFdHTnBRbGRXYTNKdWVHNUliekZKTlU4ck56TkdlRUp5VW1GUFEwRllWWGRuWjBaNFRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVkNhR05aQ21keFNXYzRWbmR5WkZOR01XdFdWRTkwVUZCR1RsTnJkMGgzV1VSV1VqQnFRa0puZDBadlFWVXpPVkJ3ZWpGWmEwVmFZalZ4VG1wd1MwWlhhWGhwTkZrS1drUTRkMGgzV1VSV1VqQlNRVkZJTDBKQ1ZYZEZORVZTV1c1S2NGbFhOVUZhUjFadldWY3hiR05wTldwaU1qQjNURUZaUzB0M1dVSkNRVWRFZG5wQlFncEJVVkZsWVVoU01HTklUVFpNZVRsdVlWaFNiMlJYU1hWWk1qbDBUREo0ZGxveWJIVk1NamxvWkZoU2IwMURORWREYVhOSFFWRlJRbWMzT0hkQlVXZEZDa2xCZDJWaFNGSXdZMGhOTmt4NU9XNWhXRkp2WkZkSmRWa3lPWFJNTW5oMldqSnNkVXd5T1doa1dGSnZUVWxIUzBKbmIzSkNaMFZGUVdSYU5VRm5VVU1LUWtoM1JXVm5RalJCU0ZsQk0xUXdkMkZ6WWtoRlZFcHFSMUkwWTIxWFl6TkJjVXBMV0hKcVpWQkxNeTlvTkhCNVowTTRjRGR2TkVGQlFVZEpha3hFUkFvNFVVRkJRa0ZOUVZKNlFrWkJhVUZWTkRCdlZtRTBOVVZQWmpGTmFYRXhZaXRRUlVaR1J6aGhMelV4VGk5dmRuSklZM2szTkhjNVpXeG5TV2hCU2pCS0NrMTJhVkl6TURsRFYzcHZjMFE1YXk5cGNUbHpLMGxxTTNocVRtUnlkVFJuYlhObUsyVnFWakpOUVc5SFEwTnhSMU5OTkRsQ1FVMUVRVEpyUVUxSFdVTUtUVkZFU3pkcU5UZHRObXRwVEZnMllpOHdZV3RRTkZOTVpYZEVVMHR5Y0dZM1RuZ3JVR1JpWlVGbE9UVlVlVkZIZEZwS1F5dGFLek5xVkVsd09YcHZjd3A0Y0VWRFRWRkVjVkJKVVV4RWVXOXJWV1ZqYVhOWFVGWktNa2RWZUhOVU1IbDNNV2h3TTB4WmRUWXpjMlJIZUZWVFZqWTRiWE0zVVhaWFZVdHJabUZDQ2xKSGNTdHpUbXM5Q2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLIn1dfX0=',
},
],
},
dsseEnvelope: {
payload: 'aGVsbG8sIHdvcmxkIQ==',
payloadType: 'text/plain',
signatures: [
{
sig: 'MEYCIQDqKDt6197vqcbC7G+4bAz5i+b+gIEHF7b0mnZBIz6o2gIhAO4o8XWAdWYDe0F6NLui+xKIP7khoAU346gJf0S3qTZo',
keyid: '',
},
],
},
};

// Public key material for verifying the above bundle
const publicKey = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGg6Hjxt2UNiJ1kwwq5XQIIwMZnJf
Expand Down Expand Up @@ -579,6 +622,7 @@ export default {
withSigningCert: validBundleWithSigningCert,
withPublicKey: validBundleWithPublicKey,
withNoTLogEntries: validBundleWithNoTLogEntries,
withDSSETLogEntry: validBundleWithDSSETLogEntry,
},
invalid: {
badSignature: invalidBadSignature,
Expand Down
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
14 changes: 14 additions & 0 deletions packages/client/src/__tests__/tlog/verify/body.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,18 @@ describe('verifyTLogBody', () => {
});
});
});

describe('when a DSSE Bundle w/ dsse tlog entry is provided', () => {
describe('when everything is valid', () => {
const bundle = sigstore.bundleFromJSON(
bundles.dsse.valid.withDSSETLogEntry
);
const tlogEntry = bundle.verificationMaterial
?.tlogEntries[0] as sigstore.VerifiableTransparencyLogEntry;

it('returns true', () => {
expect(verifyTLogBody(tlogEntry, bundle.content)).toBe(true);
});
});
});
});
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
63 changes: 63 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,41 @@ 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
const tlogSigs = tlogEntry.spec.signatures?.map(
(signature) => signature.signature
);

// 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