Skip to content

Commit

Permalink
update signature bundle format
Browse files Browse the repository at this point in the history
Signed-off-by: Brian DeHamer <bdehamer@github.com>
  • Loading branch information
bdehamer committed Aug 16, 2022
1 parent 11ebaae commit 063edae
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 103 deletions.
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
- 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",
"payloadAlgorithm": "sha256",
"base64Signature": "MEUCIQC7Rrrjmrwdxuc2qvWiWzaoUdV8+VFv+fvDquvAGmxr3AIgaPEqQ5YvxjfeqgXYXvISzgyVA8y/Zw+G/LDYlt2RHMk="
},
"cert": "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 '.cert' | 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.base64Signature;
const cert = base64Decode(bundle.cert);
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.cert);
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.cert).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: string;
attestation: Envelope;
cert: 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.base64Signature,
},
],
};

return envelope;
const dsseBundle: DSSEBundle = {
attestationType: 'attestation/dsse',
attestation: envelope,
cert: bundle.cert,
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.payloadAlgorithm).toBe('sha256');
expect(bundle.attestation.base64Signature).toBeTruthy();
expect(bundle.cert).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
53 changes: 19 additions & 34 deletions src/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,22 @@ 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;
export interface SigstoreBundle {
attestationType: string;
attestation: Record<string, string>;
cert: string;
bundle?: RekorBundle;
}

export interface RekorBundle {
signedEntryTimestamp: string;
payload: RekorPayload;
}

export interface RekorPayload {
body: object;
integratedTime: number;
logIndex: number;
logID: string;
Expand All @@ -54,7 +46,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 +90,21 @@ export class Signer {
`https://rekor.sigstore.dev/api/v1/log/entries/${entry.uuid}`
);

const signedPayload: SignedPayload = {
base64Signature: signature,
const bundle: SigstoreBundle = {
attestationType: 'attestation/blob',
attestation: {
payloadHash: digest,
payloadAlgorithm: 'sha256',
base64Signature: signature,
},
cert: b64Certificate,
bundle: entryToBundle(entry),
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 +124,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,
},
};
}
4 changes: 2 additions & 2 deletions src/sigstore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ limitations under the License.
import { KeyLike } from 'crypto';
import { Fulcio, Rekor } from './client';
import identity, { Provider } from './identity';
import { SignedPayload, Signer } from './sign';
import { Signer, SigstoreBundle } from './sign';
import { Verifier } from './verify';

export interface SignOptions {
Expand All @@ -40,7 +40,7 @@ type IdentityProviderOptions = Pick<
export async function sign(
payload: Buffer,
options: SignOptions = {}
): Promise<SignedPayload> {
): Promise<SigstoreBundle> {
const fulcio = new Fulcio({ baseURL: options.fulcioBaseURL });
const rekor = new Rekor({ baseURL: options.rekorBaseURL });
const idps = configureIdentityProviders(options);
Expand Down

0 comments on commit 063edae

Please sign in to comment.