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

update signature bundle format #46

Merged
merged 2 commits into from
Aug 18, 2022
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
19 changes: 5 additions & 14 deletions .github/workflows/build-sign-verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ jobs:
npm pack
- name: Sign package
run: |
./bin/sigstore.js sign sigstore-0.0.0.tgz > artifact.sig
./bin/sigstore.js sign sigstore-0.0.0.tgz > bundle.sigstore
Copy link
Member

Choose a reason for hiding this comment

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

Very nice! 🙌

- name: Archive package
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3
with:
Expand All @@ -92,13 +92,8 @@ jobs:
- name: Archive signature
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3
with:
name: signature
path: artifact.sig
- name: Archive certificate
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3
with:
name: certificate
path: signingcert.pem
name: bundle
path: bundle.sigstore

verify-signature:
name: Verify Signature
Expand All @@ -121,14 +116,10 @@ jobs:
- name: Download signature
uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741 # v3
with:
name: signature
- name: Download certificate
uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741 # v3
with:
name: certificate
name: bundle
- name: Compile sigstore
run: |
npm run build
- name: Verify artifact signature
run: |
./bin/sigstore.js verify sigstore-0.0.0.tgz artifact.sig signingcert.pem
./bin/sigstore.js verify sigstore-0.0.0.tgz bundle.sigstore
28 changes: 21 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,20 +75,34 @@ https://rekor.sigstore.dev/api/v1/log/entries/43553c769cd0bd99aee4350d2e78ca3fb0

You should see that a browser window is opened to the Sigstore OAuth page.
After authenticating with one of the available idenity providers, a signature
will be generated and written to the file named "signature".
bundle will be generated and written to the file named "signature".

```
$ cat signature
$ cat signature | jq

MEUCIQC7Rrrjmrwdxuc2qvWiWzaoUdV8+VFv+fvDquvAGmxr3AIgaPEqQ5YvxjfeqgXYXvISzgyVA8y/Zw+G/LDYlt2RHMk=
{
"attestationType": "attestation/blob",
"attestation": {
"payloadHash": "3ad055f2b0d850290fbe0ce63f8c60adf492901c5950ac01e0893ebb47bea4d8",
"payloadHashAlgorithm": "sha256",
"signature": "MEUCIQC7Rrrjmrwdxuc2qvWiWzaoUdV8+VFv+fvDquvAGmxr3AIgaPEqQ5YvxjfeqgXYXvISzgyVA8y/Zw+G/LDYlt2RHMk="
},
"certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNvakNDQWllZ0F3SUJBZ0lVQkswdTBRdWF3dkVJWm82YS9keGVzbEthZU9jd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpJd09ERTJNVFEwTVRJd1doY05Nakl3T0RFMk1UUTFNVEl3V2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVFRWlrR2hsMXdzSzFLMStSYTBQRU9SQzh5SXE4bTBWM0VXSSsKbDIrY2w3aUFyM0xrc292Nk5qUmtyc3VjUVVpMmRmSi9uUlZlYi9rREw1WTJYbS9VTGFPQ0FVWXdnZ0ZDTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVnNUtlCk9JWnRvRktnd3ZKVjZSSHB6M2hWcTFjd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0h3WURWUjBSQVFIL0JCVXdFNEVSWW5KcFlXNUFaR1ZvWVcxbGNpNWpiMjB3TEFZS0t3WUJCQUdEdnpBQgpBUVFlYUhSMGNITTZMeTluYVhSb2RXSXVZMjl0TDJ4dloybHVMMjloZFhSb01JR0xCZ29yQmdFRUFkWjVBZ1FDCkJIMEVld0I1QUhjQUNHQ1M4Q2hTLzJoRjBkRnJKNFNjUldjWXJCWTl3empTYmVhOElnWTJiM0lBQUFHQ3B4b1YKNVFBQUJBTUFTREJHQWlFQWtSZ1kxczlQajNFbjI1d3o0aHpXbEQzeStBYXVBSmRXd2tvWFdUZmlYd2NDSVFEMgo2a1JBU1BPbzBhdDIzNy9zTVVrd1YvSEhEQ0lBNkVnZzl2eG1pUEJqbURBS0JnZ3Foa2pPUFFRREF3TnBBREJtCkFqRUF6eDBYU3hMSkdKT29OWjN6Zk02RkUxbUZZZ3daQUJIRTFBb2xFd3ZYdTdZNUdFMkU2RWxzVjRHVDR2YlkKd2FOVUFqRUFqZzhaZlA3WXR0L1RIOVBXV1hqYkhpR3laamlpMHFpVVpTZGthTVJseGx0aC9QNXpGaHdkRTN2cQpoL1Z0UUNCdwotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlDR2pDQ0FhR2dBd0lCQWdJVUFMblZpVmZuVTBickphc21Sa0hybi9VbmZhUXdDZ1lJS29aSXpqMEVBd013CktqRVZNQk1HQTFVRUNoTU1jMmxuYzNSdmNtVXVaR1YyTVJFd0R3WURWUVFERXdoemFXZHpkRzl5WlRBZUZ3MHkKTWpBME1UTXlNREEyTVRWYUZ3MHpNVEV3TURVeE16VTJOVGhhTURjeEZUQVRCZ05WQkFvVERITnBaM04wYjNKbApMbVJsZGpFZU1Cd0dBMVVFQXhNVmMybG5jM1J2Y21VdGFXNTBaWEp0WldScFlYUmxNSFl3RUFZSEtvWkl6ajBDCkFRWUZLNEVFQUNJRFlnQUU4UlZTL3lzSCtOT3Z1RFp5UEladGlsZ1VGOU5sYXJZcEFkOUhQMXZCQkgxVTVDVjcKN0xTUzdzMFppSDRuRTdIdjdwdFM2THZ2Ui9TVGs3OThMVmdNekxsSjRIZUlmRjN0SFNhZXhMY1lwU0FTcjFrUwowTi9SZ0JKei85aldDaVhubzNzd2VUQU9CZ05WSFE4QkFmOEVCQU1DQVFZd0V3WURWUjBsQkF3d0NnWUlLd1lCCkJRVUhBd013RWdZRFZSMFRBUUgvQkFnd0JnRUIvd0lCQURBZEJnTlZIUTRFRmdRVTM5UHB6MVlrRVpiNXFOanAKS0ZXaXhpNFlaRDh3SHdZRFZSMGpCQmd3Rm9BVVdNQWVYNUZGcFdhcGVzeVFvWk1pMENyRnhmb3dDZ1lJS29aSQp6ajBFQXdNRFp3QXdaQUl3UENzUUs0RFlpWllEUElhRGk1SEZLbmZ4WHg2QVNTVm1FUmZzeW5ZQmlYMlg2U0pSCm5aVTg0LzlEWmRuRnZ2eG1BakJPdDZRcEJsYzRKLzBEeHZrVENxcGNsdnppTDZCQ0NQbmpkbElCM1B1M0J4c1AKbXlnVVk3SWkyemJkQ2RsaWlvdz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQjl6Q0NBWHlnQXdJQkFnSVVBTFpOQVBGZHhIUHdqZURsb0R3eVlDaEFPLzR3Q2dZSUtvWkl6ajBFQXdNdwpLakVWTUJNR0ExVUVDaE1NYzJsbmMzUnZjbVV1WkdWMk1SRXdEd1lEVlFRREV3aHphV2R6ZEc5eVpUQWVGdzB5Ck1URXdNRGN4TXpVMk5UbGFGdzB6TVRFd01EVXhNelUyTlRoYU1Db3hGVEFUQmdOVkJBb1RESE5wWjNOMGIzSmwKTG1SbGRqRVJNQThHQTFVRUF4TUljMmxuYzNSdmNtVXdkakFRQmdjcWhrak9QUUlCQmdVcmdRUUFJZ05pQUFUNwpYZUZUNHJiM1BRR3dTNElhanRMazMvT2xucGdhbmdhQmNsWXBzWUJyNWkrNHluQjA3Y2ViM0xQME9JT1pkeGV4Clg2OWM1aVZ1eUpSUStIejA1eWkrVUYzdUJXQWxIcGlTNXNoMCtIMkdIRTdTWHJrMUVDNW0xVHIxOUw5Z2c5MmoKWXpCaE1BNEdBMVVkRHdFQi93UUVBd0lCQmpBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJSWQp3QjVma1VXbFpxbDZ6SkNoa3lMUUtzWEYrakFmQmdOVkhTTUVHREFXZ0JSWXdCNWZrVVdsWnFsNnpKQ2hreUxRCktzWEYrakFLQmdncWhrak9QUVFEQXdOcEFEQm1BakVBajFuSGVYWnArMTNOV0JOYStFRHNEUDhHMVdXZzF0Q00KV1AvV0hQcXBhVm8wamhzd2VORlpnU3MwZUU3d1lJNHFBakVBMldCOW90OThzSWtvRjN2WllkZDMvVnRXQjViOQpUTk1lYTdJeC9zdEo1VGZjTExlQUJMRTRCTkpPc1E0dm5CSEoKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==",
"signedEntryTimestamp": "MEUCIQDLVDIXVubRBoS6tXXWptNEQNUuwPpnflrd93uBCsm3QAIgPZA5pyWhPpv9hftq0dk8b7ipjNCBzM5yIaSVXyokXbU=",
"integratedTime": 1655485921,
"logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
"logIndex": 2698234
}
```

The Fulcio signing certificate will be written to a file named
`signingcert.pem`. You can inspect the contents of the signing certificate with
the following:
You'll see that the signature bundle contains the SHA256 digest of the
artifact, the signature, the signing certificate and metadata about the
entry which was made in Rekor.

You can extract and view the signing certificate with the following:
```
$ openssl x509 -in signingcert.pem -text
$ cat signature | jq --raw-output '.certificate' | base64 -d | openssl x509 -text

Certificate:
Data:
Version: 3 (0x2)
Expand Down
41 changes: 18 additions & 23 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ async function cli(args: string[]) {
await signDSSE(args[1], args[2]);
break;
case 'verify':
await verify(args[1], args[2], args[3]);
await verify(args[1], args[2]);
break;
case 'verify-dsse':
await verifyDSSE(args[1], args[2]);
await verifyDSSE(args[1]);
break;
default:
throw 'Unknown command';
Expand All @@ -43,27 +43,24 @@ const signOptions = {

async function sign(artifactPath: string) {
const buffer = fs.readFileSync(artifactPath);
const signature = await sigstore.sign(buffer, signOptions);
const cert = base64Decode(signature.cert);
await fs.writeFileSync('signingcert.pem', cert, { flag: 'wx' });
console.log(signature.base64Signature);
const bundle = await sigstore.sign(buffer, signOptions);
console.log(JSON.stringify(bundle));
}

async function signDSSE(artifactPath: string, payloadType: string) {
const buffer = fs.readFileSync(artifactPath);
const envelope = await dsse.sign(buffer, payloadType, signOptions);
console.log(JSON.stringify(envelope));
const bundle = await dsse.sign(buffer, payloadType, signOptions);
console.log(JSON.stringify(bundle));
}

async function verify(
artifactPath: string,
signaturePath: string,
certPath: string
) {
async function verify(artifactPath: string, bundlePath: string) {
const payload = fs.readFileSync(artifactPath);
const sig = fs.readFileSync(signaturePath);
const cert = fs.readFileSync(certPath);
const result = await sigstore.verify(payload, sig.toString('utf8'), cert);
const bundleFile = fs.readFileSync(bundlePath);
const bundle = JSON.parse(bundleFile.toString('utf-8'));

const sig = bundle.attestation.signature;
const cert = base64Decode(bundle.certificate);
const result = await sigstore.verify(payload, sig, cert);

if (result) {
console.error('Verified OK');
Expand All @@ -72,13 +69,11 @@ async function verify(
}
}

async function verifyDSSE(artifactPath: string, certPath: string) {
const envelope = fs.readFileSync(artifactPath);
const cert = fs.readFileSync(certPath);
const result = await dsse.verify(
JSON.parse(envelope.toString('utf-8')),
cert
);
async function verifyDSSE(bundlePath: string) {
const bundleFile = fs.readFileSync(bundlePath);
const bundle = JSON.parse(bundleFile.toString('utf-8'));
const cert = base64Decode(bundle.certificate);
const result = await dsse.verify(bundle.attestation, cert);

if (result) {
console.error('Verified OK');
Expand Down
31 changes: 18 additions & 13 deletions src/dsse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ limitations under the License.
*/
import nock from 'nock';
import * as dsse from './dsse';
import { base64Decode } from './util';
import { base64Decode, base64Encode } from './util';

describe('sign', () => {
const fulcioBaseURL = 'http://localhost:8001';
Expand Down Expand Up @@ -104,19 +104,24 @@ describe('sign', () => {
.reply(201, rekorEntry);
});

it('returns an envelope', async () => {
const envelope = await dsse.sign(payload, payloadType, options);

expect(envelope).toEqual({
payload: payload.toString('base64'),
payloadType: payloadType,
signatures: [
{
keyid: '',
sig: expect.any(String),
},
],
it('returns a DSSE bundle', async () => {
const bundle = await dsse.sign(payload, payloadType, options);

expect(bundle.attestationType).toEqual('attestation/dsse');
expect(bundle.attestation.payload).toEqual(payload.toString('base64'));
expect(bundle.attestation.payloadType).toEqual(payloadType);
expect(bundle.attestation.signatures).toHaveLength(1);
expect(bundle.attestation.signatures[0]).toEqual({
keyid: '',
sig: expect.any(String),
});
expect(bundle.certificate).toEqual(base64Encode(certificate));
expect(bundle.integratedTime).toEqual(rekorEntry[uuid].integratedTime);
expect(bundle.signedEntryTimestamp).toEqual(
rekorEntry[uuid].verification.signedEntryTimestamp
);
expect(bundle.logID).toEqual(rekorEntry[uuid].logID);
expect(bundle.logIndex).toEqual(rekorEntry[uuid].logIndex);
});
});

Expand Down
28 changes: 24 additions & 4 deletions src/dsse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,46 @@ export interface Envelope {
signatures: Signature[];
}

export interface DSSEBundle {
attestationType: 'attestation/dsse';
attestation: Envelope;
certificate: string;
signedEntryTimestamp: string;
integratedTime: number;
logIndex: number;
logID: string;
}

export async function sign(
payload: Buffer,
payloadType: string,
options: sigstore.SignOptions = {}
): Promise<Envelope> {
): Promise<DSSEBundle> {
const paeBuffer = pae(payloadType, payload);
const signedPayload = await sigstore.sign(paeBuffer, options);
const bundle = await sigstore.sign(paeBuffer, options);

const envelope: Envelope = {
payloadType: payloadType,
payload: payload.toString('base64'),
signatures: [
{
keyid: '',
sig: signedPayload.base64Signature,
sig: bundle.attestation.signature,
},
],
};

return envelope;
const dsseBundle: DSSEBundle = {
attestationType: 'attestation/dsse',
attestation: envelope,
certificate: bundle.certificate,
signedEntryTimestamp: bundle.signedEntryTimestamp,
integratedTime: bundle.integratedTime,
logIndex: bundle.logIndex,
logID: bundle.logID,
};

return dsseBundle;
}

export async function verify(
Expand Down
21 changes: 15 additions & 6 deletions src/sign.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ limitations under the License.
import nock from 'nock';
import { Fulcio, Rekor } from './client';
import { Signer } from './sign';
import { base64Encode } from './util';

describe('Signer', () => {
const fulcioBaseURL = 'http://localhost:8001';
Expand Down Expand Up @@ -129,12 +130,20 @@ describe('Signer', () => {
});

it('returns a signature bundle', async () => {
const signedPayload = await subject.sign(payload);

expect(signedPayload).toBeTruthy();
expect(signedPayload.base64Signature).toBeTruthy();
expect(signedPayload.cert).toBe(b64Cert);
expect(signedPayload.bundle).toBeDefined();
const bundle = await subject.sign(payload);

expect(bundle).toBeTruthy();
expect(bundle.attestationType).toBe('attestation/blob');
expect(bundle.attestation.payloadHash).toBeTruthy();
expect(bundle.attestation.payloadHashAlgorithm).toBe('sha256');
expect(bundle.attestation.signature).toBeTruthy();
expect(bundle.certificate).toBe(base64Encode(certificate));
expect(bundle.integratedTime).toBe(rekorEntry[uuid].integratedTime);
expect(bundle.logIndex).toBe(rekorEntry[uuid].logIndex);
expect(bundle.logID).toBe(rekorEntry[uuid].logID);
expect(bundle.signedEntryTimestamp).toBe(
rekorEntry[uuid].verification.signedEntryTimestamp
);
});
});

Expand Down
61 changes: 25 additions & 36 deletions src/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,26 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Entry, Fulcio, Rekor } from './client';
import { Fulcio, Rekor } from './client';
import { generateKeyPair, hash, signBlob } from './crypto';
import { Provider } from './identity';
import { base64Decode, base64Encode, extractJWTSubject } from './util';
import { base64Encode, extractJWTSubject } from './util';

export interface SignOptions {
fulcio: Fulcio;
rekor: Rekor;
identityProviders: Provider[];
}

export interface SignedPayload {
base64Signature: string;
cert: string;
bundle?: RekorBundle;
}

export interface RekorBundle {
export interface SigstoreBundle {
attestationType: 'attestation/blob';
attestation: {
payloadHash: string;
payloadHashAlgorithm: string;
signature: string;
};
certificate: string;
signedEntryTimestamp: string;
payload: RekorPayload;
}

export interface RekorPayload {
body: object;
integratedTime: number;
logIndex: number;
logID: string;
Expand All @@ -54,7 +50,7 @@ export class Signer {
this.identityProviders = options.identityProviders;
}

public async sign(payload: Buffer): Promise<SignedPayload> {
public async sign(payload: Buffer): Promise<SigstoreBundle> {
// Create emphemeral key pair
const keypair = generateKeyPair();

Expand Down Expand Up @@ -98,12 +94,21 @@ export class Signer {
`https://rekor.sigstore.dev/api/v1/log/entries/${entry.uuid}`
);

const signedPayload: SignedPayload = {
base64Signature: signature,
cert: b64Certificate,
bundle: entryToBundle(entry),
const bundle: SigstoreBundle = {
attestationType: 'attestation/blob',
attestation: {
payloadHash: digest,
payloadHashAlgorithm: 'sha256',
signature: signature,
},
certificate: b64Certificate,
signedEntryTimestamp: entry.verification.signedEntryTimestamp,
integratedTime: entry.integratedTime,
logID: entry.logID,
logIndex: entry.logIndex,
};
return signedPayload;

return bundle;
}

private async getIdentityToken(): Promise<string> {
Expand All @@ -123,19 +128,3 @@ export class Signer {
throw new Error(`Identity token providers failed: ${aggErrs}`);
}
}

function entryToBundle(entry: Entry): RekorBundle | undefined {
if (!entry.verification) {
return;
}

return {
signedEntryTimestamp: entry.verification.signedEntryTimestamp,
payload: {
body: JSON.parse(base64Decode(entry.body)),
integratedTime: entry.integratedTime,
logIndex: entry.logIndex,
logID: entry.logID,
},
};
}
Loading