Skip to content

Commit

Permalink
generate signed bundle for provenance (#8)
Browse files Browse the repository at this point in the history
Signed-off-by: Brian DeHamer <[email protected]>
  • Loading branch information
bdehamer authored Oct 12, 2023
1 parent 8a5a3d2 commit 09f0f1f
Show file tree
Hide file tree
Showing 10 changed files with 63,742 additions and 18,419 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ jobs:
test-action:
name: GitHub Actions Test
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write

steps:
- name: Checkout
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TYPESCRIPT_DEFAULT_STYLE: prettier
VALIDATE_JSCPD: false
FILTER_REGEX_EXCLUDE: ./dist/.*
97 changes: 95 additions & 2 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
*/

import * as core from '@actions/core'
import * as github from '@actions/github'
import { mockFulcio, mockRekor, mockTSA } from '@sigstore/mock'
import nock from 'nock'
import {
FULCIO_INTERNAL_URL,
FULCIO_PUBLIC_GOOD_URL,
REKOR_PUBLIC_GOOD_URL,
TSA_INTERNAL_URL
} from '../src/endpoints'
import * as main from '../src/main'

// Mock the GitHub Actions core library
Expand All @@ -18,12 +27,86 @@ const setFailedMock = jest.spyOn(core, 'setFailed')
const runMock = jest.spyOn(main, 'run')

describe('action', () => {
const originalEnv = process.env
const originalContext = { ...github.context }

// Fake an OIDC token
const subject = '[email protected]'
const oidcPayload = { sub: subject, iss: '' }
const oidcToken = `.${Buffer.from(JSON.stringify(oidcPayload)).toString(
'base64'
)}.}`

beforeEach(() => {
jest.clearAllMocks()

// Mock OIDC token endpoint
const tokenURL = 'https://token.url'

nock(tokenURL)
.get('/')
.query({ audience: 'sigstore' })
.reply(200, { value: oidcToken })

process.env = {
...originalEnv,
ACTIONS_ID_TOKEN_REQUEST_URL: tokenURL,
ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token'
}
})

describe('when a subject digest is provided', () => {
beforeEach(() => {
afterEach(() => {
// Restore the original environment
process.env = originalEnv

// Restore the original github.context
setGHContext(originalContext)
})

describe('when the repository is private', () => {
beforeEach(async () => {
// Set the repository visibility to private.
setGHContext({ payload: { repository: { visibility: 'private' } } })

await mockFulcio({ baseURL: FULCIO_INTERNAL_URL, strict: false })
await mockTSA({ baseURL: TSA_INTERNAL_URL })

getInputMock.mockImplementation((name: string): string => {
switch (name) {
case 'subject-digest':
return 'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
case 'subject-name':
return 'subject'
default:
return ''
}
})
})

it('invokes the action w/o error', async () => {
await main.run()

expect(runMock).toHaveReturned()
expect(debugMock).toHaveBeenNthCalledWith(
1,
expect.stringMatching('private')
)
expect(debugMock).toHaveBeenNthCalledWith(
2,
expect.stringMatching('https://in-toto.io/Statement/v1')
)
expect(setFailedMock).not.toHaveBeenCalled()
})
})

describe('when the repository is public', () => {
beforeEach(async () => {
// Set the repository visibility to public.
setGHContext({ payload: { repository: { visibility: 'public' } } })

await mockFulcio({ baseURL: FULCIO_PUBLIC_GOOD_URL, strict: false })
await mockRekor({ baseURL: REKOR_PUBLIC_GOOD_URL })

getInputMock.mockImplementation((name: string): string => {
switch (name) {
case 'subject-digest':
Expand All @@ -42,6 +125,10 @@ describe('action', () => {
expect(runMock).toHaveReturned()
expect(debugMock).toHaveBeenNthCalledWith(
1,
expect.stringMatching('public')
)
expect(debugMock).toHaveBeenNthCalledWith(
2,
expect.stringMatching('https://in-toto.io/Statement/v1')
)
expect(setFailedMock).not.toHaveBeenCalled()
Expand All @@ -65,3 +152,9 @@ describe('action', () => {
})
})
})

// Stubbing the GitHub context is a bit tricky. We need to use
// `Object.defineProperty` because `github.context` is read-only.
function setGHContext(context: object): void {
Object.defineProperty(github, 'context', { value: context })
}
29 changes: 10 additions & 19 deletions __tests__/sign.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { mockFulcio, mockRekor, mockTSA } from '@sigstore/mock'
import assert from 'assert'
import nock from 'nock'
import {
FULCIO_INTERNAL_URL,
Expand Down Expand Up @@ -67,11 +66,8 @@ describe('signProvenance', () => {
)

// Should have a certificate
assert(
bundle.verificationMaterial.content.$case === 'x509CertificateChain'
)
expect(
bundle.verificationMaterial.content.x509CertificateChain.certificates
bundle.verificationMaterial.x509CertificateChain?.certificates
).toHaveLength(1)

// Should have one tlog entry
Expand All @@ -83,15 +79,14 @@ describe('signProvenance', () => {
).toHaveLength(0)

// Should have a signature
assert(bundle.content.$case === 'dsseEnvelope')
expect(bundle.content.dsseEnvelope.signatures).toHaveLength(1)
expect(bundle.dsseEnvelope?.signatures).toHaveLength(1)

// Should contain the provenance
expect(bundle.content.dsseEnvelope.payloadType).toEqual(
expect(bundle.dsseEnvelope?.payloadType).toEqual(
'application/vnd.in-toto+json'
)
expect(bundle.content.dsseEnvelope.payload).toEqual(
Buffer.from(JSON.stringify(provenance))
expect(bundle.dsseEnvelope?.payload).toEqual(
Buffer.from(JSON.stringify(provenance)).toString('base64')
)
})
})
Expand All @@ -112,11 +107,8 @@ describe('signProvenance', () => {
)

// Should have a certificate
assert(
bundle.verificationMaterial.content.$case === 'x509CertificateChain'
)
expect(
bundle.verificationMaterial.content.x509CertificateChain.certificates
bundle.verificationMaterial.x509CertificateChain?.certificates
).toHaveLength(1)

// Should have zero tlog entriies
Expand All @@ -128,15 +120,14 @@ describe('signProvenance', () => {
).toHaveLength(1)

// Should have a signature
assert(bundle.content.$case === 'dsseEnvelope')
expect(bundle.content.dsseEnvelope.signatures).toHaveLength(1)
expect(bundle.dsseEnvelope?.signatures).toHaveLength(1)

// Should contain the provenance
expect(bundle.content.dsseEnvelope.payloadType).toEqual(
expect(bundle.dsseEnvelope?.payloadType).toEqual(
'application/vnd.in-toto+json'
)
expect(bundle.content.dsseEnvelope.payload).toEqual(
Buffer.from(JSON.stringify(provenance))
expect(bundle.dsseEnvelope?.payload).toEqual(
Buffer.from(JSON.stringify(provenance)).toString('base64')
)
})
})
Expand Down
Loading

0 comments on commit 09f0f1f

Please sign in to comment.