├── .github
└── workflows
│ ├── build.yml
│ ├── ghpages.yml
│ ├── publish.yml
│ └── spec-prod.yml
├── .gitignore
├── .gitmodules
├── .note.xml
├── .pr-preview.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── IETF-WG-charter.md
├── LICENSE.md
├── Makefile
├── README.md
├── draft-yasskin-dispatch-web-packaging.md
├── draft-yasskin-http-origin-signed-responses.md
├── draft-yasskin-httpbis-origin-signed-exchanges-impl.md
├── draft-yasskin-wpack-use-cases.md
├── examples
└── firebase.json
├── explainers
├── anti-tracking.md
├── authoritative-site-sharing.md
├── bundle-urls-and-origins.md
├── content-addressable-bundles.md
├── integrity-signature.md
├── navigation-to-unsigned-bundles.md
├── signed-exchange-subresource-substitution.md
├── signed-exchange-subresource-subtitution-explainer.md
├── subresource-loading-opaque-origin-iframes.md
└── subresource-loading.md
├── extensions
├── primary-section.md
├── proposals
│ ├── assets
│ │ ├── dependencies-section-bundle-dep-deep.svg
│ │ ├── dependencies-section-bundle-dep.svg
│ │ ├── dependencies-section-resource-dep.svg
│ │ └── dependencies-section.org
│ └── dependencies-section.md
└── signatures-section.md
├── go.mod
├── go.sum
├── go
├── bundle
│ ├── README.md
│ ├── bundle.go
│ ├── bundle_test.go
│ ├── cmd
│ │ ├── dump-bundle
│ │ │ └── main.go
│ │ ├── gen-bundle
│ │ │ ├── fromdir.go
│ │ │ ├── fromhar.go
│ │ │ ├── fromurllist.go
│ │ │ └── main.go
│ │ └── sign-bundle
│ │ │ ├── integrityblock.go
│ │ │ ├── main.go
│ │ │ └── signedexchange.go
│ ├── countingwriter.go
│ ├── decoder.go
│ ├── encoder.go
│ ├── encoder_test.go
│ ├── har-devtools.png
│ ├── signature
│ │ ├── signer.go
│ │ ├── signer_test.go
│ │ ├── verifier.go
│ │ └── verifier_test.go
│ └── version
│ │ └── version.go
├── integrityblock
│ ├── integrityblock-signer.go
│ ├── integrityblock.go
│ ├── integrityblock_test.go
│ ├── parsed-ed25519-key-signing-strategy.go
│ ├── signing-strategy-interface.go
│ ├── testfile.wbn
│ └── webbundleid
│ │ ├── web-bundle-id.go
│ │ └── web-bundle-id_test.go
├── internal
│ ├── cbor
│ │ ├── addinfo.go
│ │ ├── decoder.go
│ │ ├── decoder_test.go
│ │ ├── deterministic.go
│ │ ├── deterministic_test.go
│ │ ├── encoder.go
│ │ ├── encoder_test.go
│ │ └── types.go
│ ├── signingalgorithm
│ │ ├── certs.go
│ │ ├── signingalgorithm.go
│ │ └── signingalgorithm_test.go
│ └── testhelper
│ │ └── cbor.go
└── signedexchange
│ ├── README.md
│ ├── certurl
│ ├── certchain-expected.cbor
│ ├── certchain.go
│ ├── certchain_test.go
│ ├── ocsp.go
│ ├── ocsp_test.go
│ ├── sct.go
│ ├── sct_test.go
│ ├── test-cert-long.pem
│ └── test-cert.pem
│ ├── cmd
│ ├── dump-certurl
│ │ └── main.go
│ ├── dump-signedexchange
│ │ └── main.go
│ ├── gen-certurl
│ │ └── main.go
│ └── gen-signedexchange
│ │ └── main.go
│ ├── internal
│ └── bigendian
│ │ ├── bigendianint.go
│ │ └── bigendianint_test.go
│ ├── mice
│ ├── mice.go
│ └── mice_test.go
│ ├── signedexchange.go
│ ├── signedexchange_test.go
│ ├── signer.go
│ ├── stateful_headers.go
│ ├── structuredheader
│ ├── parser.go
│ ├── parser_test.go
│ ├── writer.go
│ └── writer_test.go
│ ├── test-signedexchange-expected-payload-mi.bin
│ ├── verifier.go
│ └── version
│ └── version.go
├── js
├── CONTRIBUTING.md
├── bundle
│ ├── .gitignore
│ ├── .npmignore
│ ├── .prettierignore
│ ├── README.md
│ ├── bin
│ │ └── wbn.js
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── cli.ts
│ │ ├── constants.ts
│ │ ├── decoder.ts
│ │ ├── encoder.ts
│ │ └── wbn.ts
│ ├── tests
│ │ ├── cli_test.js
│ │ ├── decoder_test.js
│ │ ├── decoder_test_version_b1.cjs
│ │ ├── encoder_test.js
│ │ ├── encoder_test_version_b1.cjs
│ │ └── testdata
│ │ │ ├── encoder_test
│ │ │ ├── index.html
│ │ │ └── resources
│ │ │ │ └── style.css
│ │ │ ├── generate-testdata.sh
│ │ │ ├── header-overrides.json
│ │ │ ├── hello
│ │ │ └── hello.html
│ │ │ ├── hello_b1.wbn
│ │ │ └── hello_b2.wbn
│ └── tsconfig.json
└── sign
│ ├── .gitignore
│ ├── .npmignore
│ ├── .prettierignore
│ ├── README.md
│ ├── bin
│ ├── wbn-dump-id.js
│ └── wbn-sign.js
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ ├── cbor
│ │ ├── additionalinfo.ts
│ │ ├── deterministic.ts
│ │ └── majortype.ts
│ ├── cli-dump-id.ts
│ ├── cli-sign.ts
│ ├── signers
│ │ ├── integrity-block-signer.ts
│ │ ├── node-crypto-signing-strategy.ts
│ │ └── signing-strategy-interface.ts
│ ├── utils
│ │ ├── cli-utils.ts
│ │ ├── constants.ts
│ │ ├── read-types.d.ts
│ │ └── utils.ts
│ ├── wbn-sign.ts
│ └── web-bundle-id.ts
│ ├── tests
│ ├── cli-utils_test.js
│ ├── deterministic_test.js
│ ├── integrity-block-signer_test.js
│ └── testdata
│ │ └── unsigned.wbn
│ └── tsconfig.json
├── loading.bs
├── subresource-loading.bs
└── w3c.json
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 | on: [push, pull_request]
3 | jobs:
4 | go:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@v2
8 | - uses: actions/setup-go@v2
9 | with:
10 | go-version: "^1.16.6"
11 | - run: go test -v ./go/...
12 |
13 | js-wbn:
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | node-version: [16, 18, 20]
18 | steps:
19 | - uses: actions/checkout@v2
20 | - uses: actions/setup-node@v3
21 | with:
22 | node-version: ${{ matrix.node-version }}
23 | - run: npm --prefix ./js/bundle ci
24 | - run: npm --prefix ./js/bundle run build
25 | - run: npm --prefix ./js/bundle test
26 |
27 | js-wbn-sign:
28 | runs-on: ubuntu-latest
29 | strategy:
30 | matrix:
31 | node-version: [16, 18, 20]
32 | steps:
33 | - uses: actions/checkout@v2
34 | - uses: actions/setup-node@v3
35 | with:
36 | node-version: ${{ matrix.node-version }}
37 | - run: npm --prefix ./js/sign ci
38 | - run: npm --prefix ./js/sign run build
39 | - run: npm --prefix ./js/sign test
40 |
41 | prettier:
42 | runs-on: ubuntu-latest
43 | steps:
44 | - uses: actions/checkout@v2
45 | - uses: creyD/prettier_action@v4.3
46 | with:
47 | prettier_options: --write **/*.{js,ts}
48 | only_changed: True
49 | dry: True
50 | prettier_version: 2.7.1
51 |
--------------------------------------------------------------------------------
/.github/workflows/ghpages.yml:
--------------------------------------------------------------------------------
1 | name: "Update Editor's Copy"
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - README.md
7 | - CONTRIBUTING.md
8 | - LICENSE.md
9 | - .gitignore
10 | - "go/**"
11 | - "js/**"
12 | pull_request:
13 | paths-ignore:
14 | - README.md
15 | - CONTRIBUTING.md
16 | - LICENSE.md
17 | - .gitignore
18 | - "go/**"
19 | - "js/**"
20 |
21 | jobs:
22 | build:
23 | name: "Update Editor's Copy"
24 | runs-on: ubuntu-latest
25 | steps:
26 | - name: "Checkout"
27 | uses: actions/checkout@v2
28 |
29 | - name: "Cache Setup"
30 | id: cache-setup
31 | run: |
32 | mkdir -p "$HOME"/.cache/xml2rfc
33 | echo "::set-output name=path::$HOME/.cache/xml2rfc"
34 | date -u "+::set-output name=date::%FT%T"
35 | - name: "Cache References"
36 | uses: actions/cache@v2
37 | with:
38 | path: ${{ steps.cache-setup.outputs.path }}
39 | key: refcache-${{ steps.cache-setup.outputs.date }}
40 | restore-keys: |
41 | refcache-${{ steps.cache-setup.outputs.date }}
42 | refcache-
43 | - name: "Build Drafts"
44 | uses: martinthomson/i-d-template@v1
45 |
46 | - name: "Build Extra Pages"
47 | run: |
48 | pip3 install bikeshed
49 | bikeshed update
50 | bikeshed spec loading.bs
51 | bikeshed spec subresource-loading.bs
52 |
53 | - name: "Update GitHub Pages"
54 | uses: martinthomson/i-d-template@v1
55 | if: ${{ github.event_name == 'push' }}
56 | with:
57 | make: gh-pages
58 | env:
59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
60 |
61 | - name: "Save HTML"
62 | uses: actions/upload-artifact@v2
63 | with:
64 | path: "*.html"
65 |
66 | - name: "Save Text"
67 | uses: actions/upload-artifact@v2
68 | with:
69 | path: "*.txt"
70 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: "Publish New Draft Version"
2 |
3 | on:
4 | push:
5 | tags:
6 | - "draft-*"
7 |
8 | jobs:
9 | build:
10 | name: "Publish New Draft Version"
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: "Checkout"
14 | uses: actions/checkout@v2
15 |
16 | # See https://github.com/actions/checkout/issues/290
17 | - name: "Get Tag Annotations"
18 | run: git fetch -f origin ${{ github.ref }}:${{ github.ref }}
19 |
20 | - name: "Cache Setup"
21 | id: cache-setup
22 | run: |
23 | mkdir -p "$HOME"/.cache/xml2rfc
24 | echo "::set-output name=path::$HOME/.cache/xml2rfc"
25 | date -u "+::set-output name=date::%FT%T"
26 | - name: "Cache References"
27 | uses: actions/cache@v2
28 | with:
29 | path: ${{ steps.cache-setup.outputs.path }}
30 | key: refcache-${{ steps.date.outputs.date }}
31 | restore-keys: |
32 | refcache-${{ steps.date.outputs.date }}
33 | refcache-
34 | - name: "Upload to Datatracker"
35 | uses: martinthomson/i-d-template@v1
36 | with:
37 | make: upload
38 |
--------------------------------------------------------------------------------
/.github/workflows/spec-prod.yml:
--------------------------------------------------------------------------------
1 | name: spec-prod
2 | on:
3 | pull_request:
4 | paths-ignore:
5 | - "go/**"
6 | - "js/**"
7 | push:
8 | branches: [main]
9 | paths-ignore:
10 | - "go/**"
11 | - "js/**"
12 | jobs:
13 | main:
14 | name: Build, Validate and Deploy
15 | runs-on: ubuntu-latest
16 | strategy:
17 | max-parallel: 1
18 | matrix:
19 | include:
20 | - source: loading.bs
21 | - source: subresource-loading.bs
22 | steps:
23 | - uses: actions/checkout@v2
24 | - uses: w3c/spec-prod@v2
25 | with:
26 | GH_PAGES_BRANCH: gh-pages
27 | TOOLCHAIN: bikeshed
28 | SOURCE: ${{ matrix.source }}
29 | BUILD_FAIL_ON: warning
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.redxml
2 | draft-*.txt
3 | draft-*.html
4 | draft-*.pdf
5 | *~
6 | *.swp
7 | /*-[0-9][0-9].xml
8 | .refcache
9 | .targets.mk
10 | venv/
11 | issues.json
12 | pulls.json
13 | draft-yasskin-dispatch-web-packaging.xml
14 | /lib
15 | loading.html
16 | subresource-loading.html
17 | .vscode/
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WICG/webpackage/5262e60be64301517a51577c81421d1422fb76fd/.gitmodules
--------------------------------------------------------------------------------
/.note.xml:
--------------------------------------------------------------------------------
1 |
2 | Discussion of this document takes place on the
3 | WPACK Working Group mailing list (wpack@ietf.org),
4 | which is archived at .
5 | Source for this draft and an issue tracker can be found at
6 | .
7 |
8 |
--------------------------------------------------------------------------------
/.pr-preview.json:
--------------------------------------------------------------------------------
1 | {
2 | "src_file": "subresource-loading.bs",
3 | "type": "bikeshed",
4 | "params": {
5 | "force": 1
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | All documentation, code and communication under this repository are covered by the [W3C Code of Ethics and Professional Conduct](https://www.w3.org/Consortium/cepc/).
4 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Web Platform Incubator Community Group
2 |
3 | This repository is being used for work in the Web Platform Incubator Community Group, governed by the [W3C Community License
4 | Agreement (CLA)](http://www.w3.org/community/about/agreements/cla/). To contribute, you must join
5 | the CG.
6 |
7 | If you are not the sole contributor to a contribution (pull request), please identify all
8 | contributors in the pull request's body or in subsequent comments.
9 |
10 | To add a contributor (other than yourself, that's automatic), mark them one per line as follows:
11 |
12 | ```
13 | +@github_username
14 | ```
15 |
16 | If you added a contributor by mistake, you can remove them in a comment with:
17 |
18 | ```
19 | -@github_username
20 | ```
21 |
22 | If you are making a pull request on behalf of someone else but you had no part in designing the
23 | feature, you can remove yourself with the above syntax.
24 |
25 |
26 | # IETF
27 |
28 | This repository also relates to activities in the Internet Engineering Task Force
29 | ([IETF](https://www.ietf.org/)). All material in this repository is considered
30 | Contributions to the IETF Standards Process, as defined in the intellectual
31 | property policies of IETF currently designated as
32 | [BCP 78](https://www.rfc-editor.org/info/bcp78),
33 | [BCP 79](https://www.rfc-editor.org/info/bcp79) and the
34 | [IETF Trust Legal Provisions (TLP) Relating to IETF Documents](http://trustee.ietf.org/trust-legal-provisions.html).
35 |
36 | Any edit, commit, pull request, issue, comment or other change made to this
37 | repository constitutes Contributions to the IETF Standards Process
38 | (https://www.ietf.org/).
39 |
40 | You agree to comply with all applicable IETF policies and procedures, including,
41 | BCP 78, 79, the TLP, and the TLP rules regarding code components (e.g. being
42 | subject to a Simplified BSD License) in Contributions.
43 |
44 | # Additional Contribution Guidelines
45 |
46 | - For changes to code in `js/`, please also follow the guidelines in [js/CONTRIBUTING.md](js/CONTRIBUTING.md).
--------------------------------------------------------------------------------
/IETF-WG-charter.md:
--------------------------------------------------------------------------------
1 | # Background
2 | Webpages sometimes group multiple subresources into a single
3 | combined resource to allow cross-resource compression and to reduce the overhead
4 | of HTTP/1 requests. The W3C TAG (Technical Architecture Group) proposed a web
5 | packaging format based on multipart/* , to give web browsers visibility into
6 | the substructure of these combined resources. That has not seen deployment
7 | and HTTP/2 did not make these bundles unnecessary as was once expected.
8 |
9 | These bundles are still needed. In countries with expensive and/or unreliable
10 | mobile data, there is an established practice of sharing content and native
11 | applications peer-to-peer. Untrusted web content can generally be shared, but
12 | with the web's move to HTTPS, it is no longer possible to share web apps
13 | over these channels
14 |
15 | # WPACK
16 |
17 | The WPACK working group will develop a specification for a web packaging format
18 | that efficiently bundles multiple HTTP resources. It will also specify a way to
19 | optionally sign these resources such that a user agent can trust that they came
20 | from their claimed web origins. Key goals for WPACK are:
21 |
22 | * Efficient storage across a range of resource combinations. Three examples to
23 | be supported are: a client-generated snapshot of a complete web page, a web
24 | page's tree of JavaScript modules, and El Paquete Semanal from Cuba.
25 | * Safe web app installation after having been retrieved from a peer.
26 | * Low latency to load a subresource from a package, whether the
27 | package is signed or unsigned, and whether the package is streamed or loaded
28 | from random-access storage.
29 | * Being extensible, including to avoid cryptography that becomes obsolete.
30 | * Security and privacy properties of using bundles as close as practical to TLS
31 | 1.3 transport of the same resources. Where properties do change, the group
32 | will document exactly what changed and how content authors can compensate.
33 | * A low likelihood that the new format increases centralization or
34 | power imbalances on the web.
35 |
36 | The packaging format will also aim to achieve the secondary goals described in
37 | draft-yasskin-wpack-use-cases as long as they don't compromise or delay the
38 | above properties.
39 |
40 | The following potential goals are out of scope under this charter:
41 |
42 | * DRM
43 | * A way to distribute the private portions of a website. For example, WPACK
44 | might define a way to distribute Gmail's application but wouldn't define a way
45 | to distribute individual emails without a direct connection to Gmail's origin
46 | server.
47 | * Defining the details of how web browsers load the formats and interact with
48 | any protocols we define here.
49 | * A way to automatically discover the URL for an accessible package that
50 | includes specific content.
51 |
52 | Note that consensus is required both for changes to the current protocol
53 | mechanisms and retention of current mechanisms. In particular, because something
54 | is in the initial document set (consisting of
55 | draft-yasskin-wpack-use-cases, draft-yasskin-wpack-bundled-exchanges, and
56 | draft-yasskin-http-origin-signed-responses) does not imply that there is
57 | consensus around the feature or around how it is specified.
58 |
59 | # Relationship to Other WGs and SDOs
60 |
61 | WPACK will work with the W3C and WHATWG to identify the existing security and
62 | privacy models for the web, and to ensure those SDOs can define how this format
63 | is used by web browsers.
64 |
65 | # Milestones
66 |
67 | * Chartering + 3 Months: Working group adoption of use cases document
68 | * Chartering + 3 Months: Working group adoption of bundling document
69 | * Chartering + 3 Months: Working group adoption of security analysis document
70 | * Chartering + 3 Months: Working group adoption of privacy analysis document
71 | * Chartering + 3 Months: Working group adoption of signing document
72 | * Chartering + 18 Months: Use cases document to IESG
73 | * Chartering + 18 Months: Bundling document to IESG
74 | * Chartering + 24 Months: Security analysis document to IESG
75 | * Chartering + 24 Months: Privacy analysis document to IESG
76 | * Chartering + 24 Months: Signing document to IESG
77 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | All Reports in this Repository are licensed by Contributors under the
2 | [W3C Software and Document
3 | License](http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document). Contributions to
4 | Specifications are made under the [W3C CLA](https://www.w3.org/community/about/agreements/cla/).
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | GHPAGES_EXTRA := loading.html subresource-loading.html
2 |
3 | LIBDIR := lib
4 | include $(LIBDIR)/main.mk
5 |
6 | $(LIBDIR)/main.mk:
7 | ifneq (,$(shell grep "path *= *$(LIBDIR)" .gitmodules 2>/dev/null))
8 | git submodule sync
9 | git submodule update $(CLONE_ARGS) --init
10 | else
11 | git clone -q --depth 10 $(CLONE_ARGS) \
12 | -b main https://github.com/martinthomson/i-d-template $(LIBDIR)
13 | endif
14 |
15 | subresource-loading.html: subresource-loading.bs
16 | @ (HTTP_STATUS=$$(curl https://api.csswg.org/bikeshed/ \
17 | --output $@ \
18 | --write-out "%{http_code}" \
19 | --header "Accept: text/plain, text/html" \
20 | -F die-on=warning \
21 | -F file=@$<) && \
22 | [[ "$$HTTP_STATUS" -eq "200" ]]) || ( \
23 | echo ""; cat $@; echo ""; \
24 | rm -f $@; \
25 | exit 22 \
26 | );
27 |
--------------------------------------------------------------------------------
/examples/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "public",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ],
9 | "headers": [
10 | {
11 | "source": "**/cert.cbor",
12 | "headers": [
13 | {
14 | "key": "Content-Type",
15 | "value": "application/cert-chain+cbor"
16 | }
17 | ]
18 | },
19 | {
20 | "source": "**/*.sxg",
21 | "headers": [
22 | {
23 | "key": "Content-Type",
24 | "value": "application/signed-exchange;v=b3"
25 | },
26 | {
27 | "key": "X-Content-Type-Options",
28 | "value": "nosniff"
29 | }
30 | ]
31 | }
32 | ]
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/explainers/signed-exchange-subresource-subtitution-explainer.md:
--------------------------------------------------------------------------------
1 | Moved to [signed-exchange-subresource-substitution.md](./signed-exchange-subresource-substitution.md).
2 |
--------------------------------------------------------------------------------
/explainers/subresource-loading-opaque-origin-iframes.md:
--------------------------------------------------------------------------------
1 | # Subresource loading with Web Bundles: Support opaque origin iframes
2 |
3 | Last updated: Nov 2021
4 |
5 | This is an extension to [Subresource loading with Web Bundles]. This extension
6 | allows a bundle to include `uuid-in-package:` URL resources, which will be used to
7 | create an opaque origin iframe.
8 |
9 | ## Goals
10 |
11 | Support the use case of
12 | [WebBundles for Ad Serving](https://github.com/WICG/webpackage/issues/624).
13 |
14 | ## Extension to [Subresource loading with Web Bundles]
15 |
16 | In this section, _the explainer_ means the [Subresource loading with Web
17 | Bundles] explainer.
18 |
19 | ### Allow `uuid-in-package:` resources
20 |
21 | This extension introduces a new URL scheme `uuid-in-package:` that can be used in
22 | resource URLs in web bundles. The `uuid-in-package:` URL has the following syntax:
23 | ```
24 | ::= "uuid-in-package:"
25 | ```
26 | Where `` is a UUID as specified in
27 | [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122).
28 |
29 | In addition to the same origin subresource explained in the
30 | [`
65 |
66 |
67 | ```
68 |
69 | `uuid-in-package:f81d4fae-7dec-11d0-a765-00a0c91e6bf6` is loaded from the bundle, and a
70 | subframe is instantiated as an
71 | [opaque origin](https://html.spec.whatwg.org/multipage/origin.html#concept-origin-opaque)
72 | iframe.
73 |
74 | Note:
75 |
76 | - `uuid-in-package:` resource must be explicitly specified in `resources` in
77 | `
107 |
108 |
109 |
110 | ```
111 |
112 | Note:
113 | - When loading `HTTPS` resources from web bundles, the CSP restrictions must be
114 | evaluated against the resource URL, not against the bundle URL.
115 | - Loading `uuid-in-package` resources from web bundles served from HTTPS server is
116 | allowed when "\*" is set in the CSP
117 | [source expression](https://w3c.github.io/webappsec-csp/#source-expression).
118 | This is different from the CSP behavior that `data:` and `blob:` schemes are
119 | excluded from matching a policy of "\*". Loading `uuid-in-package` resources from web
120 | bundles is safer than using `data:` or `blob:` URL resources which are
121 | directly under the control of the page, because a `uuid-in-package` resource is a
122 | reference to a component of something with a globally-accessible URL. So we
123 | don't need to exclude `uuid-in-package` resources in a web bundle from matching the
124 | policy of "\*".
125 | - See an issue [#651](https://github.com/WICG/webpackage/issues/651) for the
126 | detailed motivation.
127 |
128 | ## Alternatives Considered
129 |
130 | ### urn:uuid: resources
131 | Previous version of this document used
132 | [`urn:uuid:` URLs](https://datatracker.ietf.org/doc/html/rfc4122) instead of
133 | `uuid-in-package:`. However, the `urn:` scheme is included in the
134 | [safelisted schemes](https://html.spec.whatwg.org/multipage/system-state.html#safelisted-scheme)
135 | of the HTML spec, meaning that web sites can
136 | [register a custom protocol handler](https://html.spec.whatwg.org/multipage/system-state.html#custom-handlers)
137 | that handles `urn:` scheme. To avoid the potential for conflict, this extension
138 | introduces the new `uuid-in-package:` scheme.
139 |
140 | Note that Chromium's experimental implementation currently supports only
141 | `urn:uuid:` as of M95.
142 |
143 | [subresource loading with web bundles]:
144 | https://github.com/WICG/webpackage/blob/main/explainers/subresource-loading.md
145 |
--------------------------------------------------------------------------------
/extensions/primary-section.md:
--------------------------------------------------------------------------------
1 | # Primary URL optional section
2 |
3 | This document outlines the format and usage of the "Primary URL" optional section as a part of a [WebBundle](https://wpack-wg.github.io/bundled-responses/draft-ietf-wpack-bundled-responses.html).
4 |
5 | ## Format
6 |
7 | ~~~ cddl
8 | primary = whatwg-url
9 | ~~~
10 |
11 | The "primary" section records a single URL identifying the primary URL of the
12 | bundle. The URL MUST refer to a resource with representations contained in the bundle itself.
13 |
14 | ## Usage
15 |
16 | The URL enclosed in the section identifies both a fallback when the recipient doesn't
17 | understand the bundle and a default resource inside the bundle to use when the
18 | recipient doesn't have more specific instructions. This field MAY be an empty
19 | string, although protocols using bundles MAY themselves forbid that empty value.
20 |
21 | While this section is not a part of the main spec, it is required for the
22 | 'Navigation to WebBundle' use-case as it acts as a default resource to load.
--------------------------------------------------------------------------------
/extensions/proposals/assets/dependencies-section.org:
--------------------------------------------------------------------------------
1 | # Assets source
2 |
3 | This is the original source from where SVG files are generated.
4 | This would be usuful if you want to update SVG files.
5 |
6 | SVGs files are generated with [mermaid(https://mermaid-js.github.io/mermaid/).
7 |
8 | #+begin_src mermaid :file dependencies-section-resource-dep.svg
9 | graph LR
10 | main1.html --> a1.js
11 | main1.html --> a.png
12 | main1.html --> a.css
13 | a1.js --> a2.js
14 | a1.js --> a3.js
15 | a3.js --> b1.js
16 | b1.js --> b2.js
17 | b1.js --> b3.js
18 | a3.js --> c1.js
19 | c1.js --> c2.js
20 | c1.js --> c3.js
21 | c2.js --> d1.js
22 | d1.js --> d2.js
23 | d1.js --> d3.js
24 | main2.html --> e1.js
25 | main2.html --> e.css
26 | main2.html --> e.png
27 | e1.js --> c1.js
28 | #+end_src
29 |
30 | #+RESULTS:
31 | :results:
32 | [[file:dependencies-section-resource-dep.svg]]
33 | :end:
34 |
35 |
36 | #+begin_src mermaid :file dependencies-section-bundle-dep.svg
37 | graph LR
38 | A --> B
39 | A --> C
40 | C --> D
41 | E --> C
42 | #+end_src
43 |
44 | #+RESULTS:
45 | :results:
46 | [[file:dependencies-section-bundle-dep.svg]]
47 | :end:
48 |
49 |
50 | #+begin_src mermaid :file dependencies-section-bundle-dep-deep.svg
51 | graph LR
52 | A --> B
53 | A --> C
54 | C --> D
55 | E --> C
56 | C --> F
57 | F --> G
58 | F --> I
59 | G --> H
60 | #+end_src
61 |
62 | #+RESULTS:
63 | :results:
64 | [[file:dependencies-section-bundle-dep-deep.svg]]
65 | :end:
66 |
--------------------------------------------------------------------------------
/extensions/signatures-section.md:
--------------------------------------------------------------------------------
1 | # Signatures optional section
2 |
3 | This document outlines the format and usage of the "signatures" optional section
4 | as a part of a [WebBundle](https://wpack-wg.github.io/bundled-responses/draft-ietf-wpack-bundled-responses.html).
5 |
6 |
7 | ## Parsing the signatures section
8 |
9 | The "signatures" section vouches for the resources in the bundle.
10 |
11 | The section can contain as many signatures as needed, each by some authority,
12 | and each covering an arbitrary subset of the resources in the bundle.
13 | Intermediates, including attackers, can remove signatures from the bundle
14 | without breaking the other signatures.
15 |
16 | The bundle parser's client is responsible to determine the validity and meaning
17 | of each authority's signatures. In particular, the algorithm below does not
18 | check that signatures are valid. For example, a client might:
19 |
20 | * Use the ecdsa_secp256r1_sha256 algorithm defined in Section 4.2.3 of
21 | [TLS1.3](https://datatracker.ietf.org/doc/html/rfc8446) to check the validity
22 | of any signature with an EC public key on the secp256r1 curve.
23 | * Reject all signatures by an RSA public key.
24 | * Treat an X.509 certificate with the CanSignHttpExchanges extension (Section
25 | 4.2 of [I-D.yasskin-http-origin-signed-responses][I-D.yasskin-http-origin-signed-responses])
26 | and a valid chain to a trusted root as an authority that vouches for the
27 | authenticity of resources claimed to come from that certificate's domains.
28 | * Treat an X.509 certificate with another extension or EKU as vouching that a
29 | particular analysis has run over the signed resources without finding
30 | malicious behavior.
31 |
32 | A client might also choose different behavior for those kinds of authorities and
33 | keys.
34 |
35 | ~~~ cddl
36 | signatures = [
37 | authorities: [*authority],
38 | vouched-subsets: [*{
39 | authority: index-in-authorities,
40 | sig: bstr,
41 | signed: bstr ; Expected to hold a signed-subset item.
42 | }],
43 | ]
44 | authority = augmented-certificate
45 | index-in-authorities = uint
46 |
47 | signed-subset = {
48 | validity-url: whatwg-url,
49 | auth-sha256: bstr,
50 | date: uint,
51 | expires: uint,
52 | subset-hashes: {+
53 | whatwg-url => [variants-value, +resource-integrity]
54 | },
55 | * tstr => any,
56 | }
57 | resource-integrity = (
58 | header-sha256: bstr,
59 | payload-integrity-header: tstr
60 | )
61 | ~~~
62 |
63 | The `augmented-certificate` CDDL rule comes from Section 3.3 of [I-D.yasskin-http-origin-signed-responses][I-D.yasskin-http-origin-signed-responses].
64 |
65 | To parse the signatures section, given its `sectionContents`, the `sectionOffsets`
66 | map, and the `metadata` map to fill in, the parser MUST do the following:
67 |
68 | 1. Let `signatures` be the result of parsing `sectionContents` as a CBOR item
69 | matching the `signatures` rule in the above CDDL.
70 | 1. Set `metadata["authorities"]` to the list of authorities in the first element
71 | of the `signatures` array.
72 | 1. Set `metadata["vouched-subsets"]` to the second element of the `signatures`
73 | array.
74 |
75 | ## Note
76 |
77 | This extension document doesn't follow the latest changes on the Web Bundles
78 | spec now. The content negotiation part was removed by this
79 | [PR](https://github.com/wpack-wg/bundled-responses/pull/7/). So the
80 | `variants-value` in the avove CDDL is nonsense now. (TODO: Need to change the
81 | CDDL not to use `variants-value`.)
82 |
83 | [I-D.yasskin-http-origin-signed-responses]:https://datatracker.ietf.org/doc/html/draft-yasskin-http-origin-signed-responses-09
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/WICG/webpackage
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/mrichman/hargo v0.1.2-0.20190117125451-162adce4527e
7 | github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2
8 | golang.org/x/crypto v0.31.0
9 | )
10 |
11 | require (
12 | github.com/influxdata/influxdb v1.7.6 // indirect
13 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
14 | github.com/sirupsen/logrus v1.3.0 // indirect
15 | github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
16 | golang.org/x/net v0.33.0 // indirect
17 | golang.org/x/sys v0.28.0 // indirect
18 | golang.org/x/term v0.27.0 // indirect
19 | golang.org/x/text v0.21.0 // indirect
20 | )
21 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/influxdata/influxdb v1.6.3/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
4 | github.com/influxdata/influxdb v1.7.6 h1:8mQ7A/V+3noMGCt/P9pD09ISaiz9XvgCk303UYA3gcs=
5 | github.com/influxdata/influxdb v1.7.6/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
6 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
7 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
8 | github.com/mrichman/hargo v0.1.2-0.20190117125451-162adce4527e h1:fcjfIMjVh8w+y7b5+vznUKU1qwiI5wAI9ybNNLuY3nc=
9 | github.com/mrichman/hargo v0.1.2-0.20190117125451-162adce4527e/go.mod h1:ycD51zRGXcO6ak4DnFPjHv4xzbgRU5tYyWDzbMzFYKw=
10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
12 | github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=
13 | github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
14 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
15 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
16 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
17 | github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4=
18 | github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
19 | github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
20 | github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
21 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
22 | golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
23 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
24 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
25 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
26 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
27 | golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
28 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
29 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
30 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
31 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
32 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
33 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
34 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
35 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
36 | golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
37 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
38 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
39 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
40 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
41 | gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0=
42 |
--------------------------------------------------------------------------------
/go/bundle/bundle.go:
--------------------------------------------------------------------------------
1 | package bundle
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "net/http"
8 | "net/url"
9 |
10 | "github.com/WICG/webpackage/go/bundle/version"
11 | "github.com/WICG/webpackage/go/signedexchange/certurl"
12 | )
13 |
14 | type Request struct {
15 | *url.URL
16 | http.Header
17 | }
18 |
19 | type Response struct {
20 | Status int
21 | http.Header
22 | Body []byte
23 | }
24 |
25 | func (r Response) String() string {
26 | return fmt.Sprintf("{Status: %q, Header: %v, body: %v}", r.Status, r.Header, string(r.Body))
27 | }
28 |
29 | type Exchange struct {
30 | Request
31 | Response
32 | }
33 |
34 | type Signatures struct {
35 | Authorities []*certurl.AugmentedCertificate
36 | VouchedSubsets []*VouchedSubset
37 | }
38 |
39 | type VouchedSubset struct {
40 | Authority uint64 // index in Authorities
41 | Sig []byte
42 | Signed []byte
43 | }
44 |
45 | type Bundle struct {
46 | Version version.Version
47 | PrimaryURL *url.URL
48 | Exchanges []*Exchange
49 | ManifestURL *url.URL
50 | Signatures *Signatures
51 | }
52 |
53 | // AddPayloadIntegrity encodes the exchange's payload with Merkle Integrity
54 | // content encoding, and adds `Content-Encoding` and `Digest` response headers.
55 | // It returns an identifier for the "payload-integrity-header" field of the
56 | // "resource-integrity" structure. [1]
57 | //
58 | // [1] https://wpack-wg.github.io/bundled-responses/draft-ietf-wpack-bundled-responses.html#signatures-section
59 | func (e *Exchange) AddPayloadIntegrity(ver version.Version, recordSize int) (string, error) {
60 | if e.Response.Header.Get("Digest") != "" {
61 | return "", errors.New("bundle: the exchange already has the Digest: header")
62 | }
63 |
64 | encoding := ver.MiceEncoding()
65 | var buf bytes.Buffer
66 | digest, err := encoding.Encode(&buf, e.Response.Body, recordSize)
67 | if err != nil {
68 | return "", err
69 | }
70 | e.Response.Body = buf.Bytes()
71 | e.Response.Header.Add("Content-Encoding", encoding.ContentEncoding())
72 | e.Response.Header.Add("Digest", digest)
73 | return encoding.IntegrityIdentifier(), nil
74 | }
75 |
76 | // Validate performs basic sanity checks on the bundle.
77 | func (b *Bundle) Validate() error {
78 | if b.PrimaryURL != nil {
79 | hasExchangeForPrimaryURL := false
80 | primaryURLString := b.PrimaryURL.String()
81 | for _, e := range b.Exchanges {
82 | if e.Request.URL.String() == primaryURLString {
83 | hasExchangeForPrimaryURL = true
84 | break
85 | }
86 | }
87 | if !hasExchangeForPrimaryURL {
88 | return fmt.Errorf("bundle: No exchange for primary URL %v", b.PrimaryURL)
89 | }
90 | }
91 | return nil
92 | }
93 |
--------------------------------------------------------------------------------
/go/bundle/bundle_test.go:
--------------------------------------------------------------------------------
1 | package bundle_test
2 |
3 | import (
4 | "bytes"
5 | "net/http"
6 | "net/url"
7 | "reflect"
8 | "testing"
9 |
10 | . "github.com/WICG/webpackage/go/bundle"
11 | "github.com/WICG/webpackage/go/bundle/version"
12 | "github.com/WICG/webpackage/go/internal/signingalgorithm"
13 | "github.com/WICG/webpackage/go/signedexchange/certurl"
14 | )
15 |
16 | const pemCerts = `-----BEGIN CERTIFICATE-----
17 | MIIBhjCCAS2gAwIBAgIJAOhR3xtYd5QsMAoGCCqGSM49BAMCMDIxFDASBgNVBAMM
18 | C2V4YW1wbGUub3JnMQ0wCwYDVQQKDARUZXN0MQswCQYDVQQGEwJVUzAeFw0xODEx
19 | MDUwOTA5MjJaFw0xOTEwMzEwOTA5MjJaMDIxFDASBgNVBAMMC2V4YW1wbGUub3Jn
20 | MQ0wCwYDVQQKDARUZXN0MQswCQYDVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49
21 | AwEHA0IABH1E6odXRm3+r7dMYmkJRmftx5IYHAsqgA7zjsFfCvPqL/fM4Uvi8EFu
22 | JVQM/oKEZw3foCZ1KBjo/6Tenkoj/wCjLDAqMBAGCisGAQQB1nkCARYEAgUAMBYG
23 | A1UdEQQPMA2CC2V4YW1wbGUub3JnMAoGCCqGSM49BAMCA0cAMEQCIEbxRKhlQYlw
24 | Ja+O9h7misjLil82Q82nhOtl4j96awZgAiB6xrvRZIlMtWYKdi41BTb5fX22gL9M
25 | L/twWg8eWpYeJA==
26 | -----END CERTIFICATE-----
27 | `
28 |
29 | func urlMustParse(rawurl string) *url.URL {
30 | u, err := url.Parse(rawurl)
31 | if err != nil {
32 | panic(err)
33 | }
34 | return u
35 | }
36 |
37 | func createTestBundle(t *testing.T, ver version.Version) *Bundle {
38 | bundle := &Bundle{
39 | Version: ver,
40 | Exchanges: []*Exchange{
41 | &Exchange{
42 | Request{
43 | URL: urlMustParse("https://bundle.example.com/"),
44 | },
45 | Response{
46 | Status: 200,
47 | Header: http.Header{"Content-Type": []string{"text/html"}},
48 | Body: []byte("hello, world!"),
49 | },
50 | },
51 | },
52 | Signatures: &Signatures{
53 | Authorities: createTestCerts(t),
54 | VouchedSubsets: []*VouchedSubset{
55 | &VouchedSubset{Authority: 0, Sig: []byte("sig"), Signed: []byte("sig")},
56 | },
57 | },
58 | }
59 | bundle.PrimaryURL = urlMustParse("https://bundle.example.com/")
60 | return bundle
61 | }
62 |
63 | func createTestBundleWithVariants(ver version.Version) *Bundle {
64 | primaryURL := urlMustParse("https://variants.example.com/")
65 | return &Bundle{
66 | Version: ver,
67 | PrimaryURL: primaryURL,
68 | Exchanges: []*Exchange{
69 | &Exchange{
70 | Request{URL: primaryURL},
71 | Response{
72 | Status: 200,
73 | Header: http.Header{
74 | "Content-Type": []string{"text/plain"},
75 | "Variants": []string{"Accept-Language;en;ja"},
76 | "Variant-Key": []string{"en"},
77 | },
78 | Body: []byte("Hello, world!"),
79 | },
80 | },
81 | &Exchange{
82 | Request{URL: primaryURL},
83 | Response{
84 | Status: 200,
85 | Header: http.Header{
86 | "Content-Type": []string{"text/plain"},
87 | "Variants": []string{"Accept-Language;en;ja"},
88 | "Variant-Key": []string{"ja"},
89 | },
90 | Body: []byte("こんにちは世界"),
91 | },
92 | },
93 | },
94 | }
95 | }
96 |
97 | func createTestCerts(t *testing.T) []*certurl.AugmentedCertificate {
98 | certs, err := signingalgorithm.ParseCertificates([]byte(pemCerts))
99 | if err != nil {
100 | t.Fatal(err)
101 | }
102 | var acs []*certurl.AugmentedCertificate
103 | for _, c := range certs {
104 | acs = append(acs, &certurl.AugmentedCertificate{Cert: c})
105 | }
106 | return acs
107 | }
108 |
109 | func TestWriteAndRead(t *testing.T) {
110 | for _, ver := range version.AllVersions {
111 | bundle := createTestBundle(t, ver)
112 |
113 | var buf bytes.Buffer
114 | n, err := bundle.WriteTo(&buf)
115 | if err != nil {
116 | t.Errorf("Bundle.WriteTo unexpectedly failed: %v", err)
117 | }
118 | if n != int64(buf.Len()) {
119 | t.Errorf("Bundle.WriteTo returned %d, but wrote %d bytes", n, buf.Len())
120 | }
121 |
122 | deserialized, err := Read(&buf)
123 | if err != nil {
124 | t.Errorf("Bundle.Read unexpectedly failed: %v", err)
125 | }
126 | if !reflect.DeepEqual(deserialized, bundle) {
127 | t.Errorf("got: %v\nwant: %v", deserialized, bundle)
128 | }
129 | }
130 | }
131 |
132 | func TestWriteAndReadWithVariants(t *testing.T) {
133 | for _, ver := range version.AllVersions {
134 | if !ver.SupportsVariants() {
135 | continue
136 | }
137 | bundle := createTestBundleWithVariants(ver)
138 |
139 | var buf bytes.Buffer
140 | if _, err := bundle.WriteTo(&buf); err != nil {
141 | t.Errorf("Bundle.WriteTo unexpectedly failed: %v", err)
142 | }
143 | deserialized, err := Read(&buf)
144 | if err != nil {
145 | t.Errorf("Bundle.Read unexpectedly failed: %v", err)
146 | }
147 | if !reflect.DeepEqual(deserialized, bundle) {
148 | t.Errorf("got: %v\nwant: %v", deserialized, bundle)
149 | }
150 | }
151 | }
152 |
153 | func TestB2BundleWithSpecifiedManifestURLShouldFail(t *testing.T) {
154 | bundle := createTestBundle(t, version.VersionB2)
155 | bundle.ManifestURL = urlMustParse("https://bundle.example.com/manifest")
156 | var buf bytes.Buffer
157 | _, err := bundle.WriteTo(&buf)
158 | if err == nil || err.Error() != "This version of the WebBundle does not support storing manifest URL." {
159 | t.Errorf("Bundle write should fail as version B2 does not support manifest URL.")
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/go/bundle/cmd/dump-bundle/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "flag"
6 | "fmt"
7 | "log"
8 | "mime"
9 | "os"
10 | "strings"
11 | "time"
12 |
13 | "github.com/WICG/webpackage/go/bundle"
14 | "github.com/WICG/webpackage/go/bundle/signature"
15 | "github.com/WICG/webpackage/go/integrityblock"
16 | )
17 |
18 | var (
19 | flagInput = flag.String("i", "in.webbundle", "Webbundle input file")
20 | flagDumpContentText = flag.Bool("contentText", true, "Dump response content if text")
21 | )
22 |
23 | func ReadBundleFromFile(path string) (*bundle.Bundle, error) {
24 | fi, err := os.Open(path)
25 | if err != nil {
26 | return nil, fmt.Errorf("Failed to open input file %q for reading. err: %v", path, err)
27 | }
28 | defer fi.Close()
29 |
30 | hasIntegrityBlock, err := integrityblock.WebBundleHasIntegrityBlock(fi)
31 | if err != nil {
32 | return nil, err
33 | }
34 |
35 | if hasIntegrityBlock {
36 | return nil, errors.New("dump-bundle doesn't support bundles which have been signed using integrity block.")
37 | }
38 |
39 | return bundle.Read(fi)
40 | }
41 |
42 | func DumpExchange(e *bundle.Exchange, b *bundle.Bundle, verifier *signature.Verifier) error {
43 | payload := e.Response.Body
44 | if verifier != nil {
45 | result, err := verifier.VerifyExchange(e)
46 | if err != nil {
47 | fmt.Printf("[Response verification error: %v]\n", err)
48 | } else if result != nil {
49 | payload = result.VerifiedPayload
50 | for i, auth := range b.Signatures.Authorities {
51 | if result.Authority == auth {
52 | fmt.Printf("[Signed with certificate #%d]\n", i)
53 | break
54 | }
55 | }
56 | } else {
57 | fmt.Println("[Not signed]")
58 | }
59 | }
60 | if _, err := fmt.Printf("> :url: %v\n", e.Request.URL); err != nil {
61 | return err
62 | }
63 | for k, v := range e.Request.Header {
64 | if _, err := fmt.Printf("> %v: %v\n", k, v); err != nil {
65 | return err
66 | }
67 | }
68 | if _, err := fmt.Printf("< :status: %v\n", e.Response.Status); err != nil {
69 | return err
70 | }
71 | for k, v := range e.Response.Header {
72 | if _, err := fmt.Printf("< %v: %v\n", k, v); err != nil {
73 | return err
74 | }
75 | }
76 | if _, err := fmt.Printf("< [len(Body)]: %d\n", len(e.Response.Body)); err != nil {
77 | return err
78 | }
79 | if *flagDumpContentText {
80 | ctype := e.Response.Header.Get("content-type")
81 | if isTextType(ctype) {
82 | if _, err := fmt.Print(string(payload)); err != nil {
83 | return err
84 | }
85 | if _, err := fmt.Print("\n"); err != nil {
86 | return err
87 | }
88 | } else {
89 | if _, err := fmt.Print("[non-text body]\n"); err != nil {
90 | return err
91 | }
92 | }
93 | }
94 | return nil
95 | }
96 |
97 | func isTextType(mimeType string) bool {
98 | m, _, err := mime.ParseMediaType(mimeType)
99 | if err != nil {
100 | // Since this is a dump tool, we just ignore parse errors.
101 | return false
102 | }
103 | return strings.HasPrefix(m, "text/") || m == "application/javascript"
104 | }
105 |
106 | func run() error {
107 | b, err := ReadBundleFromFile(*flagInput)
108 | if err != nil {
109 | return err
110 | }
111 |
112 | fmt.Printf("Version: %v\n", b.Version)
113 |
114 | if b.PrimaryURL != nil {
115 | fmt.Printf("Primary URL: %v\n", b.PrimaryURL)
116 | }
117 | if b.ManifestURL != nil {
118 | fmt.Printf("Manifest URL: %v\n", b.ManifestURL)
119 | }
120 |
121 | var verifier *signature.Verifier
122 | if b.Signatures != nil {
123 | fmt.Println("Signatures:")
124 | for i, ac := range b.Signatures.Authorities {
125 | fmt.Printf(" Certificate #%d:\n", i)
126 | fmt.Println(" Subject:", ac.Cert.Subject.CommonName)
127 | fmt.Println(" Valid from:", ac.Cert.NotBefore)
128 | fmt.Println(" Valid until:", ac.Cert.NotAfter)
129 | fmt.Println(" Issuer:", ac.Cert.Issuer.CommonName)
130 | }
131 | var err error
132 | verifier, err = signature.NewVerifier(b.Signatures, time.Now(), b.Version)
133 | if err != nil {
134 | fmt.Printf("Signature verification error: %v\n", err)
135 | }
136 | }
137 |
138 | for _, e := range b.Exchanges {
139 | fmt.Println()
140 | if err := DumpExchange(e, b, verifier); err != nil {
141 | return err
142 | }
143 | }
144 |
145 | return nil
146 | }
147 |
148 | func main() {
149 | flag.Parse()
150 | if err := run(); err != nil {
151 | log.Fatal(err)
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/go/bundle/cmd/gen-bundle/fromdir.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "log"
7 | "net/http"
8 | "net/url"
9 | "os"
10 | "path/filepath"
11 | "strings"
12 |
13 | "github.com/WICG/webpackage/go/bundle"
14 | )
15 |
16 | func fromDir(baseDir string, baseURL *url.URL) ([]*bundle.Exchange, error) {
17 | es := []*bundle.Exchange{}
18 | err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
19 | if err != nil {
20 | return err
21 | }
22 | url, err := convertPathToURL(path, baseDir, baseURL)
23 | if err != nil {
24 | return err
25 | }
26 | if info.IsDir() {
27 | // For a directory, create an entry only if it contains index.html
28 | // (otherwise http.ServeFile generates a directory list).
29 | if _, err := os.Stat(filepath.Join(path, "index.html")); err != nil {
30 | if os.IsNotExist(err) {
31 | return nil
32 | }
33 | return fmt.Errorf("Stat(%s) failed. err: %v", path, err)
34 | }
35 | if !strings.HasSuffix(url, "/") {
36 | url += "/"
37 | }
38 | }
39 | e, err := createExchange(path, url)
40 | if err != nil {
41 | return err
42 | }
43 | es = append(es, e)
44 | return nil
45 | })
46 | if err != nil {
47 | return nil, fmt.Errorf("Error walking the path %s. err: %v", baseDir, err)
48 | }
49 | return es, nil
50 | }
51 |
52 | func convertPathToURL(path string, baseDir string, baseURL *url.URL) (string, error) {
53 | relPath, err := filepath.Rel(baseDir, path)
54 | if err != nil {
55 | return "", fmt.Errorf("Cannot make relative path for %q: %v", path, err)
56 | }
57 | var result *url.URL
58 | if baseURL != nil {
59 | result, err = baseURL.Parse(filepath.ToSlash(relPath))
60 | } else {
61 | result, err = url.Parse(filepath.ToSlash(relPath))
62 | }
63 | if err != nil {
64 | return "", fmt.Errorf("Failed to construct URL for %s. err: %v", path, err)
65 | }
66 | return result.String(), nil
67 | }
68 |
69 | // responseWriter implements http.ResponseWriter.
70 | type responseWriter struct {
71 | bytes.Buffer
72 | status int
73 | header http.Header
74 | }
75 |
76 | func newResponseWriter() *responseWriter {
77 | return &responseWriter{header: make(http.Header)}
78 | }
79 |
80 | func (w *responseWriter) Header() http.Header {
81 | return w.header
82 | }
83 |
84 | func (w *responseWriter) WriteHeader(statusCode int) {
85 | w.status = statusCode
86 | }
87 |
88 | // createExchange creates a bundle.Exchange whose request URL is url
89 | // and response body is the contents of the file. Internally, it uses
90 | // http.ServeFile to generate a realistic HTTP response for the file.
91 | func createExchange(file string, url string) (*bundle.Exchange, error) {
92 | req, err := http.NewRequest("GET", url, nil)
93 | if err != nil {
94 | return nil, fmt.Errorf("http.newRequest failed: %v", err)
95 | }
96 | log.Printf("Creating exchange: %s -> %s", file, req.URL)
97 |
98 | w := newResponseWriter()
99 | http.ServeFile(w, req, file)
100 |
101 | return &bundle.Exchange{
102 | Request: bundle.Request{
103 | URL: req.URL,
104 | Header: req.Header,
105 | },
106 | Response: bundle.Response{
107 | Status: w.status,
108 | Header: w.header,
109 | Body: w.Bytes(),
110 | },
111 | }, nil
112 | }
113 |
--------------------------------------------------------------------------------
/go/bundle/cmd/gen-bundle/fromhar.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "log"
9 | "net/http"
10 | "net/url"
11 | "os"
12 | "strings"
13 |
14 | "github.com/mrichman/hargo"
15 |
16 | "github.com/WICG/webpackage/go/bundle"
17 | "github.com/WICG/webpackage/go/signedexchange"
18 | )
19 |
20 | func ReadHar(r io.Reader) (*hargo.Har, error) {
21 | dec := json.NewDecoder(r)
22 | var har hargo.Har
23 | if err := dec.Decode(&har); err != nil {
24 | return nil, fmt.Errorf("Failed to parse har. err: %v", err)
25 | }
26 | return &har, nil
27 | }
28 |
29 | func ReadHarFromFile(path string) (*hargo.Har, error) {
30 | fi, err := os.Open(path)
31 | if err != nil {
32 | return nil, fmt.Errorf("Failed to open input file %q for reading. err: %v", path, err)
33 | }
34 | defer fi.Close()
35 | return ReadHar(fi)
36 | }
37 |
38 | func nvpToHeader(nvps []hargo.NVP, predBanned func(string) bool) (http.Header, error) {
39 | h := make(http.Header)
40 | for _, nvp := range nvps {
41 | // Drop HTTP/2 pseudo headers.
42 | if strings.HasPrefix(nvp.Name, ":") {
43 | continue
44 | }
45 | if predBanned(nvp.Name) {
46 | log.Printf("Dropping banned header: %q", nvp.Name)
47 | continue
48 | }
49 | h.Add(nvp.Name, nvp.Value)
50 | }
51 | return h, nil
52 | }
53 |
54 | func contentToBody(c *hargo.Content) ([]byte, error) {
55 | if c.Encoding == "base64" {
56 | return base64.StdEncoding.DecodeString(c.Text)
57 | }
58 | return []byte(c.Text), nil
59 | }
60 |
61 | func fromHar(harPath string) ([]*bundle.Exchange, error) {
62 | har, err := ReadHarFromFile(harPath)
63 | if err != nil {
64 | return nil, err
65 | }
66 |
67 | es := []*bundle.Exchange{}
68 | hasVariants := make(map[string]bool)
69 |
70 | for _, e := range har.Log.Entries {
71 | log.Printf("Processing entry: %q", e.Request.URL)
72 |
73 | parsedUrl, err := url.Parse(e.Request.URL) // TODO(kouhei): May be this should e.Respose.RedirectURL?
74 | if err != nil {
75 | return nil, fmt.Errorf("Failed to parse request URL %q. err: %v", e.Request.URL, err)
76 | }
77 | reqh, err := nvpToHeader(e.Request.Headers, signedexchange.IsStatefulRequestHeader)
78 | if err != nil {
79 | return nil, fmt.Errorf("Failed to parse request header for the request %q. err: %v", e.Request.URL, err)
80 | }
81 | resh, err := nvpToHeader(e.Response.Headers, signedexchange.IsUncachedHeader)
82 | if err != nil {
83 | return nil, fmt.Errorf("Failed to parse response header for the request %q. err: %v", e.Request.URL, err)
84 | }
85 | body, err := contentToBody(&e.Response.Content)
86 | if err != nil {
87 | return nil, fmt.Errorf("Failed to extract body from response content for the request %q. err: %v", e.Request.URL, err)
88 | }
89 |
90 | if e.Request.Method != http.MethodGet {
91 | log.Printf("Dropping the entry: non-GET request method (%s)", e.Request.Method)
92 | continue
93 | }
94 | if e.Response.Status < 100 || e.Response.Status > 999 {
95 | log.Printf("Dropping the entry: invalid response status (%d)", e.Response.Status)
96 | continue
97 | }
98 |
99 | // Allow multiple entries for single URL only if all responses have
100 | // Variants: header.
101 | _, thisHasVariants := resh["Variants"]
102 | othersHaveVariants, hasMultipleEntries := hasVariants[parsedUrl.String()]
103 | if hasMultipleEntries && (!thisHasVariants || !othersHaveVariants) {
104 | log.Printf("Dropping the entry: exchange for this URL already exists, and has no Variants header")
105 | continue
106 | }
107 | hasVariants[parsedUrl.String()] = thisHasVariants
108 |
109 | e := &bundle.Exchange{
110 | Request: bundle.Request{
111 | URL: parsedUrl,
112 | Header: reqh,
113 | },
114 | Response: bundle.Response{
115 | Status: e.Response.Status,
116 | Header: resh,
117 | Body: body,
118 | },
119 | }
120 | es = append(es, e)
121 | }
122 |
123 | return es, nil
124 | }
125 |
--------------------------------------------------------------------------------
/go/bundle/cmd/gen-bundle/fromurllist.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "io/ioutil"
7 | "log"
8 | "net/http"
9 | "net/url"
10 | "os"
11 | "strings"
12 |
13 | "github.com/WICG/webpackage/go/bundle"
14 | )
15 |
16 | func fromURLList(urlListFile string) ([]*bundle.Exchange, error) {
17 | input, err := os.Open(urlListFile)
18 | if err != nil {
19 | return nil, fmt.Errorf("Failed to open %q: %v", urlListFile, err)
20 | }
21 | defer input.Close()
22 | scanner := bufio.NewScanner(input)
23 |
24 | es := []*bundle.Exchange{}
25 | seen := make(map[string]struct{})
26 | for scanner.Scan() {
27 | rawURL := strings.TrimSpace(scanner.Text())
28 | // Skip blank lines and comments.
29 | if len(rawURL) == 0 || rawURL[0] == '#' {
30 | continue
31 | }
32 | if _, ok := seen[rawURL]; ok {
33 | log.Printf("Skipping duplicated URL %q", rawURL)
34 | continue
35 | }
36 |
37 | seen[rawURL] = struct{}{}
38 | log.Printf("Processing %q", rawURL)
39 |
40 | parsedURL, err := url.Parse(rawURL)
41 | if err != nil {
42 | return nil, fmt.Errorf("Failed to parse URL %q: %v", rawURL, err)
43 | }
44 | resp, err := http.Get(rawURL)
45 | if err != nil {
46 | return nil, fmt.Errorf("Failed to fetch %q: %v", rawURL, err)
47 | }
48 | defer resp.Body.Close()
49 | body, err := ioutil.ReadAll(resp.Body)
50 | if err != nil {
51 | return nil, fmt.Errorf("Error reading response body of %q: %v", rawURL, err)
52 | }
53 | e := &bundle.Exchange{
54 | Request: bundle.Request{
55 | URL: parsedURL,
56 | },
57 | Response: bundle.Response{
58 | Status: resp.StatusCode,
59 | Header: resp.Header,
60 | Body: body,
61 | },
62 | }
63 | es = append(es, e)
64 | }
65 | if err := scanner.Err(); err != nil {
66 | return nil, fmt.Errorf("Error reading %q: %v", urlListFile, err)
67 | }
68 |
69 | return es, nil
70 | }
71 |
--------------------------------------------------------------------------------
/go/bundle/cmd/gen-bundle/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "log"
7 | "net/url"
8 | "os"
9 | "strings"
10 |
11 | "github.com/WICG/webpackage/go/bundle"
12 | "github.com/WICG/webpackage/go/bundle/version"
13 | )
14 |
15 | type headerArgs []string
16 |
17 | func (h *headerArgs) String() string {
18 | return fmt.Sprintf("%v", *h)
19 | }
20 |
21 | func (h *headerArgs) Set(value string) error {
22 | *h = append(*h, value)
23 | return nil
24 | }
25 |
26 | var (
27 | flagVersion = flag.String("version", string(version.VersionB2), "The webbundle format version. Possible values are: 'b1' and 'b2'")
28 | flagHar = flag.String("har", "", "HTTP Archive (HAR) input file")
29 | flagDir = flag.String("dir", "", "Input directory")
30 | flagBaseURL = flag.String("baseURL", "", "Base URL (used with -dir)")
31 | flagPrimaryURL = flag.String("primaryURL", "", "Primary URL")
32 | flagManifestURL = flag.String("manifestURL", "", "Manifest URL")
33 | flagOutput = flag.String("o", "out.wbn", "Webbundle output file")
34 | flagURLList = flag.String("URLList", "", "URL list file")
35 | flagIgnoreErrors = flag.Bool("ignoreErrors", false, "Do not reject invalid input arguments")
36 |
37 | flagHeaderOverride = headerArgs{}
38 | )
39 |
40 | func init() {
41 | flag.Var(&flagHeaderOverride, "headerOverride", "Set additional response header, replacing any existing values")
42 | }
43 |
44 | func main() {
45 | flag.Parse()
46 |
47 | ver, ok := version.Parse(*flagVersion)
48 | if !ok {
49 | log.Fatalf("Error: failed to parse version %q\n", *flagVersion)
50 | }
51 | if *flagPrimaryURL == "" && ver.HasPrimaryURLFieldInHeader() {
52 | fmt.Fprintln(os.Stderr, "Please specify -primaryURL or change your bundle version to a newer one.")
53 | flag.Usage()
54 | return
55 | }
56 | var parsedPrimaryURL *url.URL
57 | var err error
58 | if len(*flagPrimaryURL) > 0 {
59 | parsedPrimaryURL, err = url.Parse(*flagPrimaryURL)
60 | if err != nil {
61 | log.Fatalf("Failed to parse primary URL. err: %v", err)
62 | }
63 | }
64 |
65 | var parsedManifestURL *url.URL
66 | if len(*flagManifestURL) > 0 {
67 | parsedManifestURL, err = url.Parse(*flagManifestURL)
68 | if err != nil {
69 | log.Fatalf("Failed to parse manifest URL. err: %v", err)
70 | }
71 | }
72 |
73 | b := &bundle.Bundle{Version: ver, PrimaryURL: parsedPrimaryURL, ManifestURL: parsedManifestURL}
74 |
75 | if *flagHar != "" {
76 | if *flagBaseURL != "" {
77 | fmt.Fprintln(os.Stderr, "Warning: -baseURL is ignored when input is HAR.")
78 | }
79 | es, err := fromHar(*flagHar)
80 | if err != nil {
81 | log.Fatal(err)
82 | }
83 | b.Exchanges = es
84 | } else if *flagDir != "" {
85 | var parsedBaseURL *url.URL
86 | if len(*flagBaseURL) > 0 {
87 | parsedBaseURL, err = url.Parse(*flagBaseURL)
88 | if err != nil {
89 | log.Fatalf("Failed to parse base URL. err: %v", err)
90 | }
91 | }
92 | es, err := fromDir(*flagDir, parsedBaseURL)
93 | if err != nil {
94 | log.Fatal(err)
95 | }
96 | b.Exchanges = es
97 | } else if *flagURLList != "" {
98 | es, err := fromURLList(*flagURLList)
99 | if err != nil {
100 | log.Fatal(err)
101 | }
102 | b.Exchanges = es
103 | } else {
104 | fmt.Fprintln(os.Stderr, "Please specify one of -har, -dir, or -URLList.")
105 | flag.Usage()
106 | return
107 | }
108 |
109 | for _, h := range flagHeaderOverride {
110 | chunks := strings.SplitN(h, ":", 2)
111 | for _, e := range b.Exchanges {
112 | e.Response.Header.Set(chunks[0], strings.TrimSpace(chunks[1]))
113 | }
114 | }
115 |
116 | if !*flagIgnoreErrors {
117 | if err := b.Validate(); err != nil {
118 | log.Fatal(err)
119 | }
120 | }
121 |
122 | fo, err := os.OpenFile(*flagOutput, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
123 | if err != nil {
124 | log.Fatalf("Failed to open output file %q for writing. err: %v", *flagOutput, err)
125 | }
126 | defer fo.Close()
127 | if _, err := b.WriteTo(fo); err != nil {
128 | log.Fatalf("Failed to write exchange. err: %v", err)
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/go/bundle/cmd/sign-bundle/integrityblock.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/ed25519"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "io/ioutil"
9 | "os"
10 |
11 | "github.com/WICG/webpackage/go/integrityblock"
12 | "github.com/WICG/webpackage/go/integrityblock/webbundleid"
13 | "github.com/WICG/webpackage/go/internal/cbor"
14 | "github.com/WICG/webpackage/go/internal/signingalgorithm"
15 | )
16 |
17 | func writeOutput(bundleFile io.ReadSeeker, integrityBlockBytes []byte, originalIntegrityBlockOffset int64, signedBundleFile *os.File) error {
18 | signedBundleFile.Write(integrityBlockBytes)
19 |
20 | // Move the file pointer to the start of the web bundle bytes.
21 | bundleFile.Seek(originalIntegrityBlockOffset, io.SeekStart)
22 |
23 | // io.Copy() will do chunked read/write under the hood
24 | _, err := io.Copy(signedBundleFile, bundleFile)
25 | if err != nil {
26 | return err
27 | }
28 | return nil
29 | }
30 |
31 | func readPublicEd25519KeyFromFile(path string) (ed25519.PublicKey, error) {
32 | pubkeytext, err := ioutil.ReadFile(path)
33 | if err != nil {
34 | return nil, errors.New("SignIntegrityBlock: Unable to read the public key.")
35 | }
36 | pubKey, err := signingalgorithm.ParsePublicKey(pubkeytext)
37 |
38 | ed25519pubKey, ok := pubKey.(ed25519.PublicKey)
39 | if !ok {
40 | return nil, errors.New("SignIntegrityBlock: Public key is not Ed25519 type.")
41 | }
42 | return ed25519pubKey, nil
43 | }
44 |
45 | func readAndParseEd25519PrivateKey(path string) (ed25519.PrivateKey, error) {
46 | privKey, err := readPrivateKeyFromFile(path)
47 | if err != nil {
48 | return nil, errors.New("SignIntegrityBlock: Unable to read the private key.")
49 | }
50 |
51 | ed25519privKey, ok := privKey.(ed25519.PrivateKey)
52 | if !ok {
53 | return nil, errors.New("SignIntegrityBlock: Private key is not Ed25519 type.")
54 | }
55 | return ed25519privKey, nil
56 | }
57 |
58 | func DumpWebBundleIdFromPrivateKey() error {
59 | ed25519privKey, err := readAndParseEd25519PrivateKey(*dumpIdFlagPrivateKey)
60 | if err != nil {
61 | return err
62 | }
63 |
64 | webBundleId := webbundleid.GetWebBundleId(ed25519privKey.Public().(ed25519.PublicKey))
65 | fmt.Printf("Web Bundle ID: %s\n", webBundleId)
66 | return nil
67 | }
68 |
69 | func DumpWebBundleIdFromPublicKey() error {
70 | ed25519pubKey, err := readPublicEd25519KeyFromFile(*dumpIdFlagPublicKey)
71 | if err != nil {
72 | return err
73 | }
74 |
75 | webBundleId := webbundleid.GetWebBundleId(ed25519pubKey)
76 | fmt.Printf("Web Bundle ID: %s\n", webBundleId)
77 | return nil
78 | }
79 |
80 | func DumpWebBundleId() error {
81 | if isFlagPassed(dumpWebBundleIdCmd, flagNamePublicKey) {
82 | return DumpWebBundleIdFromPublicKey()
83 | } else {
84 | return DumpWebBundleIdFromPrivateKey()
85 | }
86 | }
87 |
88 | // SignWithIntegrityBlockWithCmdFlags is just a wrapper function for `SignWithIntegrityBlock`
89 | // function containing the actual logic so that it can be easily exported without having
90 | // to rely on reading and writing to files specified to be read from the CMD tool flags.
91 | func SignWithIntegrityBlockWithCmdFlags(signingStrategy integrityblock.ISigningStrategy) error {
92 | if *ibFlagInput == *ibFlagOutput {
93 | return errors.New("SignIntegrityBlock: Input and output file cannot be the same.")
94 | }
95 |
96 | bundleFile, err := os.Open(*ibFlagInput)
97 | if err != nil {
98 | return err
99 | }
100 | defer bundleFile.Close()
101 |
102 | signedBundleFile, err := os.Create(*ibFlagOutput)
103 | if err != nil {
104 | return err
105 | }
106 | defer signedBundleFile.Close()
107 |
108 | return SignWithIntegrityBlock(bundleFile, signedBundleFile, signingStrategy)
109 | }
110 |
111 | // SignWithIntegrityBlock creates a CBOR integrity block containing a signature
112 | // matching the hash of the web bundle read from `bundleFileIn`. Finally it
113 | // writes the new signed web bundle into `bundleFileOut`. More details can be
114 | // found in [Integrity Block Explainer](https://github.com/WICG/webpackage/blob/main/explainers/integrity-signature.md).
115 | func SignWithIntegrityBlock(bundleFileIn, bundleFileOut *os.File, signingStrategy integrityblock.ISigningStrategy) error {
116 | integrityBlock, offset, err := integrityblock.ObtainIntegrityBlock(bundleFileIn)
117 | if err != nil {
118 | return err
119 | }
120 |
121 | webBundleHash, err := integrityblock.ComputeWebBundleSha512(bundleFileIn, offset)
122 | if err != nil {
123 | return err
124 | }
125 |
126 | ibs := integrityblock.IntegrityBlockSigner{
127 | SigningStrategy: signingStrategy,
128 | WebBundleHash: webBundleHash,
129 | IntegrityBlock: integrityBlock,
130 | }
131 |
132 | ed25519publicKey, err := ibs.SigningStrategy.GetPublicKey()
133 | if err != nil {
134 | return err
135 | }
136 |
137 | signatureAttributes := integrityblock.GenerateSignatureAttributesWithPublicKey(ed25519publicKey)
138 |
139 | err = ibs.SignAndAddNewSignature(ed25519publicKey, signatureAttributes)
140 | if err != nil {
141 | return err
142 | }
143 |
144 | // Update the integrity block bytes with the new integrity block.
145 | integrityBlockBytes, err := integrityBlock.CborBytes()
146 | if err != nil {
147 | return err
148 | }
149 |
150 | err = cbor.Deterministic(integrityBlockBytes)
151 | if err != nil {
152 | return err
153 | }
154 |
155 | webBundleId := webbundleid.GetWebBundleId(ed25519publicKey)
156 | fmt.Println("Web Bundle ID: " + webBundleId)
157 |
158 | return writeOutput(bundleFileIn, integrityBlockBytes, offset, bundleFileOut)
159 | }
160 |
--------------------------------------------------------------------------------
/go/bundle/cmd/sign-bundle/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "flag"
6 | "fmt"
7 | "log"
8 | "os"
9 | "time"
10 |
11 | "github.com/WICG/webpackage/go/integrityblock"
12 | )
13 |
14 | const (
15 | signaturesSectionSubCmdName = "signatures-section"
16 | integrityBlockSubCmdName = "integrity-block"
17 | dumpWebBundleIdSubCmdName = "dump-id"
18 | )
19 |
20 | var (
21 | signedExchangesCmd = flag.NewFlagSet(signaturesSectionSubCmdName, flag.ExitOnError)
22 | sxgFlagInput = signedExchangesCmd.String("i", "in.wbn", "Webbundle input file")
23 | sxgFlagOutput = signedExchangesCmd.String("o", "out.wbn", "Webbundle output file")
24 | sxgFlagCertificate = signedExchangesCmd.String("certificate", "cert.cbor", "Certificate chain CBOR file")
25 | sxgFlagPrivateKey = signedExchangesCmd.String("privateKey", "cert-key.pem", "Private key PEM file")
26 | sxgFlagValidityUrl = signedExchangesCmd.String("validityUrl", "https://example.com/resource.validity.msg", "The URL where resource validity info is hosted at.")
27 | sxgFlagDate = signedExchangesCmd.String("date", "", "Datetime for the signature in RFC3339 format (2006-01-02T15:04:05Z). (default: current time)")
28 | sxgFlagExpire = signedExchangesCmd.Duration("expire", 1*time.Hour, "Validity duration of the signature")
29 | sxgFlagMIRecordSize = signedExchangesCmd.Int("miRecordSize", 4096, "Record size of Merkle Integrity Content Encoding")
30 | )
31 |
32 | var (
33 | integrityBlockCmd = flag.NewFlagSet(integrityBlockSubCmdName, flag.ExitOnError)
34 | ibFlagInput = integrityBlockCmd.String("i", "in.wbn", "Webbundle input file")
35 | ibFlagOutput = integrityBlockCmd.String("o", "out.wbn", "Webbundle output file")
36 | ibFlagPrivateKey = integrityBlockCmd.String("privateKey", "privatekey.pem", "Private key PEM file")
37 | )
38 |
39 | const flagNamePublicKey = "publicKey"
40 |
41 | var (
42 | dumpWebBundleIdCmd = flag.NewFlagSet(dumpWebBundleIdSubCmdName, flag.ExitOnError)
43 | dumpIdFlagPrivateKey = dumpWebBundleIdCmd.String("privateKey", "privatekey.pem", "Private key PEM file whose corresponding Web Bundle ID is wanted.")
44 | dumpIdFlagPublicKey = dumpWebBundleIdCmd.String(flagNamePublicKey, "", "Public key PEM file whose corresponding Web Bundle ID is wanted.")
45 | )
46 |
47 | // isFlagPassed is a helper function to check if the given flag was provided. Note that this needs to be called after flag.Parse.
48 | func isFlagPassed(flags *flag.FlagSet, name string) bool {
49 | found := false
50 | flags.Visit(func(f *flag.Flag) {
51 | if f.Name == name {
52 | found = true
53 | }
54 | })
55 | return found
56 | }
57 |
58 | func run() error {
59 | switch os.Args[1] {
60 |
61 | case signaturesSectionSubCmdName:
62 | signedExchangesCmd.Parse(os.Args[2:])
63 | return SignExchanges()
64 |
65 | case integrityBlockSubCmdName:
66 | integrityBlockCmd.Parse(os.Args[2:])
67 |
68 | // TODO(sonkkeli): Add parsing for the new `signingStrategy` flag and
69 | // make another switch case for the different types of signing.
70 |
71 | ed25519privKey, err := readAndParseEd25519PrivateKey(*ibFlagPrivateKey)
72 | if err != nil {
73 | return err
74 | }
75 |
76 | var bss integrityblock.ISigningStrategy = integrityblock.NewParsedEd25519KeySigningStrategy(ed25519privKey)
77 | return SignWithIntegrityBlockWithCmdFlags(bss)
78 |
79 | case dumpWebBundleIdSubCmdName:
80 | dumpWebBundleIdCmd.Parse(os.Args[2:])
81 | return DumpWebBundleId()
82 |
83 | default:
84 | return errors.New(fmt.Sprintf("Unknown subcommand, try '%s', '%s' or '%s'", signaturesSectionSubCmdName, integrityBlockSubCmdName, dumpWebBundleIdSubCmdName))
85 | }
86 | }
87 |
88 | func main() {
89 | if err := run(); err != nil {
90 | log.Fatal(err)
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/go/bundle/cmd/sign-bundle/signedexchange.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto"
5 | "crypto/ecdsa"
6 | "errors"
7 | "fmt"
8 | "io/ioutil"
9 | "net/url"
10 | "os"
11 | "time"
12 |
13 | "github.com/WICG/webpackage/go/bundle"
14 | "github.com/WICG/webpackage/go/bundle/signature"
15 | "github.com/WICG/webpackage/go/internal/signingalgorithm"
16 | "github.com/WICG/webpackage/go/signedexchange/certurl"
17 | )
18 |
19 | func readCertChainFromFile(path string) (certurl.CertChain, error) {
20 | fi, err := os.Open(path)
21 | if err != nil {
22 | return nil, err
23 | }
24 | defer fi.Close()
25 | return certurl.ReadCertChain(fi)
26 | }
27 |
28 | func readPrivateKeyFromFile(path string) (crypto.PrivateKey, error) {
29 | privkeytext, err := ioutil.ReadFile(path)
30 | if err != nil {
31 | return nil, err
32 | }
33 | return signingalgorithm.ParsePrivateKey(privkeytext)
34 | }
35 |
36 | func readBundleFromFile(path string) (*bundle.Bundle, error) {
37 | fi, err := os.Open(path)
38 | if err != nil {
39 | return nil, err
40 | }
41 | defer fi.Close()
42 | return bundle.Read(fi)
43 | }
44 |
45 | func writeBundleToFile(b *bundle.Bundle, path string) error {
46 | fo, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
47 | if err != nil {
48 | return err
49 | }
50 | defer fo.Close()
51 | _, err = b.WriteTo(fo)
52 | return err
53 | }
54 |
55 | func addSignature(b *bundle.Bundle, signer *signature.Signer) error {
56 | for _, e := range b.Exchanges {
57 | if !signer.CanSignForURL(e.Request.URL) {
58 | continue
59 | }
60 | payloadIntegrityHeader, err := e.AddPayloadIntegrity(b.Version, *sxgFlagMIRecordSize)
61 | if err != nil {
62 | return err
63 | }
64 | if err := signer.AddExchange(e, payloadIntegrityHeader); err != nil {
65 | return err
66 | }
67 | }
68 |
69 | newSignatures, err := signer.UpdateSignatures(b.Signatures)
70 | if err != nil {
71 | return err
72 | }
73 | b.Signatures = newSignatures
74 | return nil
75 | }
76 |
77 | func SignExchanges() error {
78 | privKey, err := readPrivateKeyFromFile(*sxgFlagPrivateKey)
79 | if err != nil {
80 | return fmt.Errorf("%s: %v", *sxgFlagPrivateKey, err)
81 | }
82 |
83 | if _, ok := privKey.(*ecdsa.PrivateKey); !ok {
84 | return errors.New("Private key is not ECDSA type.")
85 | }
86 |
87 | certs, err := readCertChainFromFile(*sxgFlagCertificate)
88 | if err != nil {
89 | return fmt.Errorf("%s: %v", *sxgFlagCertificate, err)
90 | }
91 |
92 | validityUrl, err := url.Parse(*sxgFlagValidityUrl)
93 | if err != nil {
94 | return fmt.Errorf("failed to parse validity URL %q: %v", *sxgFlagValidityUrl, err)
95 | }
96 |
97 | var date time.Time
98 | if *sxgFlagDate == "" {
99 | date = time.Now()
100 | } else {
101 | var err error
102 | date, err = time.Parse(time.RFC3339, *sxgFlagDate)
103 | if err != nil {
104 | return fmt.Errorf("failed to parse date %q: %v", *sxgFlagDate, err)
105 | }
106 | }
107 |
108 | b, err := readBundleFromFile(*sxgFlagInput)
109 | if err != nil {
110 | return fmt.Errorf("%s: %v", *sxgFlagInput, err)
111 | }
112 |
113 | signer, err := signature.NewSigner(b.Version, certs, privKey, validityUrl, date, *sxgFlagExpire)
114 | if err != nil {
115 | return err
116 | }
117 |
118 | if err := addSignature(b, signer); err != nil {
119 | return err
120 | }
121 |
122 | if err := writeBundleToFile(b, *sxgFlagOutput); err != nil {
123 | return fmt.Errorf("%s: %v", *sxgFlagOutput, err)
124 | }
125 | return nil
126 | }
127 |
--------------------------------------------------------------------------------
/go/bundle/countingwriter.go:
--------------------------------------------------------------------------------
1 | package bundle
2 |
3 | import (
4 | "io"
5 | )
6 |
7 | // CoutingWriter counts number of writes written
8 | type CountingWriter struct {
9 | w io.Writer
10 | Written int64
11 | }
12 |
13 | var _ = io.Writer(&CountingWriter{})
14 | var _ = io.ReaderFrom(&CountingWriter{})
15 |
16 | func NewCountingWriter(w io.Writer) *CountingWriter {
17 | return &CountingWriter{
18 | w: w,
19 | Written: 0,
20 | }
21 | }
22 |
23 | func (cw *CountingWriter) Write(p []byte) (n int, err error) {
24 | n, err = cw.w.Write(p)
25 | cw.Written += int64(n)
26 | return
27 | }
28 |
29 | func (cw *CountingWriter) ReadFrom(r io.Reader) (n int64, err error) {
30 | if rf, ok := cw.w.(io.ReaderFrom); ok {
31 | n, err = rf.ReadFrom(r)
32 | cw.Written += n
33 | return
34 | }
35 |
36 | buf := make([]byte, 32*1024)
37 | n = 0
38 | for {
39 | var nr int
40 | nr, err = r.Read(buf)
41 | if err != nil {
42 | return
43 | }
44 |
45 | var nw int
46 | nw, err = cw.w.Write(buf[:nr])
47 | if err != nil {
48 | n += int64(nw)
49 | cw.Written += n
50 | return
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/go/bundle/encoder_test.go:
--------------------------------------------------------------------------------
1 | package bundle
2 |
3 | import (
4 | "net/http"
5 | "net/url"
6 | "reflect"
7 | "testing"
8 |
9 | "github.com/WICG/webpackage/go/bundle/version"
10 | "github.com/WICG/webpackage/go/internal/testhelper"
11 | )
12 |
13 | func urlMustParse(rawurl string) *url.URL {
14 | u, err := url.Parse(rawurl)
15 | if err != nil {
16 | panic(err)
17 | }
18 | return u
19 | }
20 |
21 | func TestVariants(t *testing.T) {
22 | variants, err := parseVariants("Accept-Encoding;gzip;br, Accept-Language;en;fr;ja")
23 | if err != nil {
24 | t.Errorf("parseListOfStringLists unexpectedly failed: %v", err)
25 | }
26 | if nk, err := variants.numberOfPossibleKeys(); nk != 6 || err != nil {
27 | t.Errorf("numberOfPossibleKeys: got: (%v, %v) want: (%v, %v)", nk, err, 6, nil)
28 | }
29 |
30 | cases := []struct {
31 | index int
32 | variantKey []string
33 | }{
34 | {0, []string{"gzip", "en"}},
35 | {1, []string{"gzip", "fr"}},
36 | {2, []string{"gzip", "ja"}},
37 | {3, []string{"br", "en"}},
38 | {4, []string{"br", "fr"}},
39 | {5, []string{"br", "ja"}},
40 | {-1, []string{"gzip", "es"}},
41 | {-1, []string{}},
42 | {-1, []string{"gzip"}},
43 | {-1, []string{"gzip", "en", "foo"}},
44 | }
45 | for _, c := range cases {
46 | if i := variants.indexInPossibleKeys(c.variantKey); i != c.index {
47 | t.Errorf("indexInPossibleKeys: got: %v want: %v", i, c.index)
48 | }
49 |
50 | if c.index != -1 {
51 | key := variants.possibleKeyAt(c.index)
52 | if !reflect.DeepEqual(key, c.variantKey) {
53 | t.Errorf("possibleKeyAt(%d): got: %v\nwant: %v", c.index, key, c.variantKey)
54 | }
55 | }
56 | }
57 | }
58 |
59 | func TestIndexSectionWithVariants(t *testing.T) {
60 | url := urlMustParse("https://example.com/")
61 | variants := []string{"Accept-Encoding;gzip;br, Accept-Language;en;fr"}
62 | is := &indexSection{}
63 | is.addExchange(
64 | &Exchange{
65 | Request{URL: url},
66 | Response{Header: http.Header{
67 | "Variants": variants,
68 | "Variant-Key": []string{"gzip;fr, br;en"},
69 | }},
70 | }, 20, 2)
71 | is.addExchange(
72 | &Exchange{
73 | Request{URL: url},
74 | Response{Header: http.Header{
75 | "Variants": variants,
76 | "Variant-Key": []string{"gzip;en"},
77 | }},
78 | }, 10, 1)
79 | is.addExchange(
80 | &Exchange{
81 | Request{URL: url},
82 | Response{Header: http.Header{
83 | "Variants": variants,
84 | "Variant-Key": []string{"br;fr"},
85 | }},
86 | }, 30, 3)
87 | if err := is.Finalize(version.VersionB1); err != nil {
88 | t.Fatal(err)
89 | }
90 |
91 | want := `map["https://example.com/":["Accept-Encoding;gzip;br, Accept-Language;en;fr" 10 1 20 2 20 2 30 3]]`
92 |
93 | got, err := testhelper.CborBinaryToReadableString(is.bytes)
94 | if err != nil {
95 | t.Fatal(err)
96 | }
97 | if got != want {
98 | t.Errorf("got: %s\nwant: %s", got, want)
99 | }
100 | }
101 |
102 | func TestIndexSectionMultipleResourcesPerURL(t *testing.T) {
103 | url := urlMustParse("https://example.com/")
104 | is := &indexSection{}
105 | is.addExchange(
106 | &Exchange{
107 | Request{URL: url},
108 | Response{},
109 | }, 10, 1)
110 | is.addExchange(
111 | &Exchange{
112 | Request{URL: url},
113 | Response{},
114 | }, 20, 1)
115 | err := is.Finalize(version.VersionB2)
116 | if err.Error() != "This WebBundle version 'b2' does not support variants, so we cannot have multiple resources per URL." {
117 | t.Fatal(err)
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/go/bundle/har-devtools.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WICG/webpackage/5262e60be64301517a51577c81421d1422fb76fd/go/bundle/har-devtools.png
--------------------------------------------------------------------------------
/go/bundle/signature/signer_test.go:
--------------------------------------------------------------------------------
1 | package signature_test
2 |
3 | import (
4 | "bytes"
5 | "net/http"
6 | "net/url"
7 | "reflect"
8 | "testing"
9 | "time"
10 |
11 | "github.com/WICG/webpackage/go/bundle"
12 | . "github.com/WICG/webpackage/go/bundle/signature"
13 | "github.com/WICG/webpackage/go/bundle/version"
14 | "github.com/WICG/webpackage/go/internal/signingalgorithm"
15 | "github.com/WICG/webpackage/go/signedexchange/certurl"
16 | )
17 |
18 | const (
19 | // A certificate for "example.org"
20 | pemCerts = `-----BEGIN CERTIFICATE-----
21 | MIIBhjCCAS2gAwIBAgIJAOhR3xtYd5QsMAoGCCqGSM49BAMCMDIxFDASBgNVBAMM
22 | C2V4YW1wbGUub3JnMQ0wCwYDVQQKDARUZXN0MQswCQYDVQQGEwJVUzAeFw0xODEx
23 | MDUwOTA5MjJaFw0xOTEwMzEwOTA5MjJaMDIxFDASBgNVBAMMC2V4YW1wbGUub3Jn
24 | MQ0wCwYDVQQKDARUZXN0MQswCQYDVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49
25 | AwEHA0IABH1E6odXRm3+r7dMYmkJRmftx5IYHAsqgA7zjsFfCvPqL/fM4Uvi8EFu
26 | JVQM/oKEZw3foCZ1KBjo/6Tenkoj/wCjLDAqMBAGCisGAQQB1nkCARYEAgUAMBYG
27 | A1UdEQQPMA2CC2V4YW1wbGUub3JnMAoGCCqGSM49BAMCA0cAMEQCIEbxRKhlQYlw
28 | Ja+O9h7misjLil82Q82nhOtl4j96awZgAiB6xrvRZIlMtWYKdi41BTb5fX22gL9M
29 | L/twWg8eWpYeJA==
30 | -----END CERTIFICATE-----
31 | `
32 | pemPrivateKey = `-----BEGIN EC PRIVATE KEY-----
33 | MHcCAQEEIEMac81NMjwO4pQ2IGKZ3UdymYtnFAXEjKdvAdEx4DQwoAoGCCqGSM49
34 | AwEHoUQDQgAEfUTqh1dGbf6vt0xiaQlGZ+3HkhgcCyqADvOOwV8K8+ov98zhS+Lw
35 | QW4lVAz+goRnDd+gJnUoGOj/pN6eSiP/AA==
36 | -----END EC PRIVATE KEY-----`
37 |
38 | miRecordSize = 4096
39 | validityURL = "https://example.org/resource.validity"
40 | )
41 |
42 | var signatureDate = time.Date(2018, 1, 31, 17, 13, 20, 0, time.UTC)
43 | var signatureDuration = 1 * time.Hour
44 |
45 | var expectedSig = []byte{
46 | 0xc1, 0xd3, 0xcc, 0x2d, 0x42, 0x52, 0xc5, 0x6f, 0xe4, 0x3b, 0x60, 0x44,
47 | 0x87, 0xe6, 0xeb, 0x9c, 0x90, 0xab, 0xaa, 0x5d, 0x91, 0x55, 0x00, 0x0b,
48 | 0x00, 0x59, 0x04, 0x5a, 0xe4, 0xdf, 0x15, 0x15,
49 | }
50 |
51 | func urlMustParse(rawurl string) *url.URL {
52 | u, err := url.Parse(rawurl)
53 | if err != nil {
54 | panic(err)
55 | }
56 | return u
57 | }
58 |
59 | func createTestCertChain(t *testing.T) certurl.CertChain {
60 | certs, err := signingalgorithm.ParseCertificates([]byte(pemCerts))
61 | if err != nil {
62 | t.Fatal(err)
63 | }
64 | chain, err := certurl.NewCertChain(certs, []byte("dummy ocsp"), nil)
65 | if err != nil {
66 | t.Fatal(err)
67 | }
68 | return chain
69 | }
70 |
71 | func createTestSigner(t *testing.T) *Signer {
72 | certChain := createTestCertChain(t)
73 |
74 | privKey, err := signingalgorithm.ParsePrivateKey([]byte(pemPrivateKey))
75 | if err != nil {
76 | t.Fatal(err)
77 | }
78 |
79 | validityUrl := urlMustParse(validityURL)
80 |
81 | signer, err := NewSigner(version.VersionB1, certChain, privKey, validityUrl, signatureDate, signatureDuration)
82 | if err != nil {
83 | t.Fatalf("Failed to create Signer: %v", err)
84 | }
85 | return signer
86 | }
87 |
88 | func TestCanSignForURL(t *testing.T) {
89 | signer := createTestSigner(t)
90 |
91 | if !signer.CanSignForURL(urlMustParse("https://example.org/index.html")) {
92 | t.Error("CanSignFor unexpectedly returned false for https://example.org/index.html")
93 | }
94 | if signer.CanSignForURL(urlMustParse("https://example.com/index.html")) {
95 | t.Error("CanSignFor unexpectedly returned true for https://example.com/index.html")
96 | }
97 | }
98 |
99 | func TestSignatureGeneration(t *testing.T) {
100 | signer := createTestSigner(t)
101 | signer.Algorithm = &signingalgorithm.MockSigningAlgorithm{}
102 |
103 | e := &bundle.Exchange{
104 | bundle.Request{
105 | URL: urlMustParse("https://example.org/index.html"),
106 | },
107 | bundle.Response{
108 | Status: 200,
109 | Header: http.Header{"Content-Type": []string{"text/html"}},
110 | Body: []byte("hello, world!"),
111 | },
112 | }
113 | integrity, err := e.AddPayloadIntegrity(signer.Version, miRecordSize)
114 | if err != nil {
115 | t.Fatalf("AddPayloadIntegrity failed: %v", err)
116 | }
117 |
118 | if err := signer.AddExchange(e, integrity); err != nil {
119 | t.Fatalf("signer.AddExchange failed: %v", err)
120 | }
121 |
122 | signatures, err := signer.UpdateSignatures(nil)
123 | if err != nil {
124 | t.Fatalf("signer.UpdateSignatures failed: %v", err)
125 | }
126 |
127 | if len(signatures.Authorities) != 1 {
128 | t.Fatalf("Unexpected size of signatures.Authorities: %d", len(signatures.Authorities))
129 | }
130 | expectedCerts := createTestCertChain(t)
131 | if !reflect.DeepEqual(signatures.Authorities[0], expectedCerts[0]) {
132 | t.Errorf("signatures.Authorities[0]:\n got: %v\n want: %v", signatures.Authorities[0], expectedCerts[0])
133 | }
134 |
135 | if len(signatures.VouchedSubsets) != 1 {
136 | t.Fatalf("Unexpected size of signatures.VouchedSubsets: %d", len(signatures.VouchedSubsets))
137 | }
138 | vh := signatures.VouchedSubsets[0]
139 | if vh.Authority != 0 {
140 | t.Errorf("Authority: got %d, want %d", vh.Authority, 0)
141 | }
142 | if !bytes.Equal(vh.Sig, expectedSig) {
143 | t.Errorf("Sig:\n got: %v\n want: %v", vh.Sig, expectedSig)
144 | }
145 |
146 | headerSha256, err := e.Response.HeaderSha256()
147 | if err != nil {
148 | t.Fatalf("HeaderSha256 failed: %v", err)
149 | }
150 | expectedSigned, err := (&SignedSubset{
151 | ValidityUrl: urlMustParse(validityURL),
152 | AuthSha256: expectedCerts[0].CertSha256(),
153 | Date: signatureDate,
154 | Expires: signatureDate.Add(signatureDuration),
155 | SubsetHashes: map[string]*ResponseHashes{
156 | e.Request.URL.String(): &ResponseHashes{
157 | VariantsValue: nil,
158 | Hashes: []*ResourceIntegrity{
159 | &ResourceIntegrity{headerSha256, "digest/mi-sha256-03"},
160 | },
161 | },
162 | },
163 | }).Encode()
164 |
165 | if !bytes.Equal(vh.Signed, expectedSigned) {
166 | t.Errorf("Signed:\n got: %v\n want: %v", vh.Signed, expectedSigned)
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/go/bundle/signature/verifier_test.go:
--------------------------------------------------------------------------------
1 | package signature_test
2 |
3 | import (
4 | "bytes"
5 | "net/http"
6 | "testing"
7 | "time"
8 |
9 | "github.com/WICG/webpackage/go/bundle"
10 | . "github.com/WICG/webpackage/go/bundle/signature"
11 | )
12 |
13 | func createTestSignedBundle(t *testing.T) *bundle.Bundle {
14 | signer := createTestSigner(t)
15 |
16 | e := &bundle.Exchange{
17 | bundle.Request{
18 | URL: urlMustParse("https://example.org/index.html"),
19 | },
20 | bundle.Response{
21 | Status: 200,
22 | Header: http.Header{"Content-Type": []string{"text/html"}},
23 | Body: []byte("hello, world!"),
24 | },
25 | }
26 | integrity, err := e.AddPayloadIntegrity(signer.Version, miRecordSize)
27 | if err != nil {
28 | t.Fatalf("AddPayloadIntegrity failed: %v", err)
29 | }
30 | if err := signer.AddExchange(e, integrity); err != nil {
31 | t.Fatalf("signer.AddExchange failed: %v", err)
32 | }
33 | signatures, err := signer.UpdateSignatures(nil)
34 | if err != nil {
35 | t.Fatalf("signer.UpdateSignatures failed: %v", err)
36 | }
37 |
38 | return &bundle.Bundle{
39 | Version: signer.Version,
40 | Exchanges: []*bundle.Exchange{e},
41 | Signatures: signatures,
42 | }
43 | }
44 |
45 | func TestVerification(t *testing.T) {
46 | b := createTestSignedBundle(t)
47 |
48 | verifier, err := NewVerifier(b.Signatures, signatureDate, b.Version)
49 | if err != nil {
50 | t.Fatalf("NewVerifier failed: %v", err)
51 | }
52 | result, err := verifier.VerifyExchange(b.Exchanges[0])
53 | if err != nil {
54 | t.Fatalf("VerifyExchange failed: %v", err)
55 | }
56 | if result.Authority != b.Signatures.Authorities[0] {
57 | t.Fatalf("VerifyExchange: unexpected result.Authority %v", result.Authority)
58 | }
59 | if !bytes.Equal(result.VerifiedPayload, []byte("hello, world!")) {
60 | t.Fatalf("VerifyExchange: unexpected result.VerifiedPayload %v", result.VerifiedPayload)
61 | }
62 | }
63 |
64 | func TestSignatureNotYetValid(t *testing.T) {
65 | b := createTestSignedBundle(t)
66 |
67 | if _, err := NewVerifier(b.Signatures, signatureDate.Add(-1*time.Second), b.Version); err == nil {
68 | t.Error("NewVerifier should fail")
69 | }
70 | }
71 |
72 | func TestSignatureExpired(t *testing.T) {
73 | b := createTestSignedBundle(t)
74 |
75 | if _, err := NewVerifier(b.Signatures, signatureDate.Add(signatureDuration+time.Second), b.Version); err == nil {
76 | t.Error("NewVerifier should fail")
77 | }
78 | }
79 |
80 | func TestSignatureVerificationFailure(t *testing.T) {
81 | b := createTestSignedBundle(t)
82 |
83 | // Mutate the signature.
84 | b.Signatures.VouchedSubsets[0].Sig[3] ^= 1
85 |
86 | if _, err := NewVerifier(b.Signatures, signatureDate, b.Version); err == nil {
87 | t.Error("NewVerifier should fail")
88 | }
89 | }
90 |
91 | func TestExchangeNotCoveredBySignature(t *testing.T) {
92 | b := createTestSignedBundle(t)
93 |
94 | // This URL is not covered by the signature.
95 | b.Exchanges[0].Request.URL = urlMustParse("https://example.org/unsigned.html")
96 |
97 | verifier, err := NewVerifier(b.Signatures, signatureDate, b.Version)
98 | if err != nil {
99 | t.Fatalf("NewVerifier failed: %v", err)
100 | }
101 | result, err := verifier.VerifyExchange(b.Exchanges[0])
102 | if err != nil {
103 | t.Errorf("VerifyExchange failed: %v", err)
104 | }
105 | if result != nil {
106 | t.Errorf("VerifyExchange unexpectedly returned a result: %v", result)
107 | }
108 | }
109 |
110 | func TestResponseHeaderVerificationFailure(t *testing.T) {
111 | b := createTestSignedBundle(t)
112 |
113 | b.Exchanges[0].Response.Status = 201
114 |
115 | verifier, err := NewVerifier(b.Signatures, signatureDate, b.Version)
116 | if err != nil {
117 | t.Fatalf("NewVerifier failed: %v", err)
118 | }
119 | if _, err := verifier.VerifyExchange(b.Exchanges[0]); err == nil {
120 | t.Error("VerifyExchange should fail")
121 | }
122 | }
123 |
124 | func TestResponsePayloadVerificationFailure(t *testing.T) {
125 | b := createTestSignedBundle(t)
126 |
127 | // Mutate the last byte of the response body.
128 | b.Exchanges[0].Response.Body[len(b.Exchanges[0].Response.Body)-1] ^= 1
129 |
130 | verifier, err := NewVerifier(b.Signatures, signatureDate, b.Version)
131 | if err != nil {
132 | t.Fatalf("NewVerifier failed: %v", err)
133 | }
134 | if _, err := verifier.VerifyExchange(b.Exchanges[0]); err == nil {
135 | t.Error("VerifyExchange should fail")
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/go/bundle/version/version.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "io"
7 |
8 | "github.com/WICG/webpackage/go/signedexchange/mice"
9 | )
10 |
11 | type Version string
12 |
13 | const (
14 | VersionB1 Version = "b1"
15 | VersionB2 Version = "b2"
16 | )
17 |
18 | var AllVersions = []Version{
19 | VersionB1,
20 | VersionB2,
21 | }
22 |
23 | // HeaderMagicBytesB1 is the CBOR encoding of the 6-item array initial byte and 8-byte bytestring initial byte, followed by 🌐📦 in UTF-8.
24 | // These bytes are for the header of b1 version.
25 | var HeaderMagicBytesB1 = []byte{0x86, 0x48, 0xf0, 0x9f, 0x8c, 0x90, 0xf0, 0x9f, 0x93, 0xa6}
26 |
27 | // HeaderMagicBytesB2 is the CBOR encoding of the 5-item array initial byte and 8-byte bytestring initial byte, followed by 🌐📦 in UTF-8.
28 | // These bytes are for the header of b2 version.
29 | var HeaderMagicBytesB2 = []byte{0x85, 0x48, 0xf0, 0x9f, 0x8c, 0x90, 0xf0, 0x9f, 0x93, 0xa6}
30 |
31 | // VersionMagicBytesB1 is the CBOR encoding of a 4-byte byte string holding an ASCII "b1" followed by two 0 bytes
32 | var VersionMagicBytesB1 = []byte{0x44, 0x62, 0x31, 0x00, 0x00}
33 |
34 | // VersionMagicBytesB2 is the CBOR encoding of a 4-byte byte string holding an ASCII "b2" followed by two 0 bytes
35 | var VersionMagicBytesB2 = []byte{0x44, 0x62, 0x32, 0x00, 0x00}
36 |
37 | func Parse(str string) (Version, bool) {
38 | switch Version(str) {
39 | case VersionB1:
40 | return VersionB1, true
41 | case VersionB2:
42 | return VersionB2, true
43 | }
44 | return "", false
45 | }
46 |
47 | func (v Version) HeaderMagicBytes() []byte {
48 | switch v {
49 | case VersionB1:
50 | return append(HeaderMagicBytesB1, VersionMagicBytesB1...)
51 | case VersionB2:
52 | return append(HeaderMagicBytesB2, VersionMagicBytesB2...)
53 | default:
54 | panic("not reached")
55 | }
56 | }
57 |
58 | func ParseMagicBytes(r io.Reader) (Version, error) {
59 | hdrMagic := make([]byte, len(HeaderMagicBytesB1))
60 | if _, err := io.ReadFull(r, hdrMagic); err != nil {
61 | return "", err
62 | }
63 | if bytes.Compare(hdrMagic, HeaderMagicBytesB1) != 0 && bytes.Compare(hdrMagic, HeaderMagicBytesB2) != 0 {
64 | return "", errors.New("bundle: unrecognized header magic")
65 | }
66 |
67 | verMagic := make([]byte, len(VersionMagicBytesB1))
68 | if _, err := io.ReadFull(r, verMagic); err != nil {
69 | return "", err
70 | }
71 | if bytes.Compare(verMagic, VersionMagicBytesB1) == 0 {
72 | if bytes.Compare(hdrMagic, HeaderMagicBytesB1) != 0 {
73 | return "", errors.New("bundle: header magic bytes does not match version magic bytes")
74 | }
75 | return VersionB1, nil
76 | }
77 | if bytes.Compare(verMagic, VersionMagicBytesB2) == 0 {
78 | if bytes.Compare(hdrMagic, HeaderMagicBytesB2) != 0 {
79 | return "", errors.New("bundle: header magic bytes does not match version magic bytes")
80 | }
81 | return VersionB2, nil
82 | }
83 | return "", errors.New("bundle: unrecognized version magic")
84 | }
85 |
86 | func (v Version) MiceEncoding() mice.Encoding {
87 | switch v {
88 | case VersionB1, VersionB2:
89 | return mice.Draft03Encoding
90 | default:
91 | panic("not reached")
92 | }
93 | }
94 |
95 | func (v Version) SignatureContextString() string {
96 | switch v {
97 | case VersionB1:
98 | return "Web Package 1 b1"
99 | case VersionB2:
100 | return "Web Package 1 b2"
101 | default:
102 | panic("not reached")
103 | }
104 | }
105 |
106 | func (v Version) HasPrimaryURLFieldInHeader() bool {
107 | return v == VersionB1
108 | }
109 |
110 | func (v Version) SupportsVariants() bool {
111 | return v == VersionB1
112 | }
113 |
114 | // TODO: consider changing this also to only version B1, as the
115 | // signatures section is not a part of the main spec anymore.
116 | // Currently returns true as both B1 and B2 can have signatures
117 | // (temporarily).
118 | func (v Version) SupportsSignatures() bool {
119 | return true
120 | }
121 |
122 | func (v Version) SupportsManifestSection() bool {
123 | return v == VersionB1
124 | }
125 |
--------------------------------------------------------------------------------
/go/integrityblock/integrityblock-signer.go:
--------------------------------------------------------------------------------
1 | package integrityblock
2 |
3 | import (
4 | "crypto/ed25519"
5 | "errors"
6 |
7 | "github.com/WICG/webpackage/go/internal/cbor"
8 | )
9 |
10 | type IntegrityBlockSigner struct {
11 | SigningStrategy ISigningStrategy
12 | WebBundleHash []byte
13 | IntegrityBlock *IntegrityBlock
14 | }
15 |
16 | // VerifyEd25519Signature verifies that the given signature can be verified with the given public key and matches the data signed.
17 | func VerifyEd25519Signature(publicKey ed25519.PublicKey, signature, dataToBeSigned []byte) (bool, error) {
18 | signatureOk := ed25519.Verify(publicKey, dataToBeSigned, signature)
19 | if !signatureOk {
20 | return signatureOk, errors.New("integrityblock: Signature verification failed.")
21 | }
22 | return signatureOk, nil
23 | }
24 |
25 | // SignAndAddNewSignature contains the main logic for generating the new signature and
26 | // prepending the integrity block's signature stack with a new integrity signature object.
27 | func (ibs *IntegrityBlockSigner) SignAndAddNewSignature(ed25519publicKey ed25519.PublicKey, signatureAttributes SignatureAttributesMap) error {
28 | integrityBlockBytes, err := ibs.IntegrityBlock.CborBytes()
29 | if err != nil {
30 | return err
31 | }
32 |
33 | // Ensure the CBOR on the integrity block follows the deterministic principles.
34 | err = cbor.Deterministic(integrityBlockBytes)
35 | if err != nil {
36 | return err
37 | }
38 |
39 | dataToBeSigned, err := GenerateDataToBeSigned(ibs.WebBundleHash, integrityBlockBytes, signatureAttributes)
40 | if err != nil {
41 | return err
42 | }
43 |
44 | signature, err := ibs.SigningStrategy.Sign(dataToBeSigned)
45 | if err != nil {
46 | return err
47 | }
48 |
49 | // Verification is done after signing to ensure that the signing was successful and that the obtained public key
50 | // is not corrupted and corresponds to the private key used for signing.
51 | VerifyEd25519Signature(ed25519publicKey, signature, dataToBeSigned)
52 |
53 | ibs.IntegrityBlock.addNewSignatureToIntegrityBlock(signatureAttributes, signature)
54 | return nil
55 | }
56 |
--------------------------------------------------------------------------------
/go/integrityblock/parsed-ed25519-key-signing-strategy.go:
--------------------------------------------------------------------------------
1 | package integrityblock
2 |
3 | import (
4 | "crypto/ed25519"
5 | )
6 |
7 | // ParsedEd25519KeySigningStrategy implementing `ISigningStrategy` is the simplest way to
8 | // sign a web bundle just by passing a parsed private key.
9 | type ParsedEd25519KeySigningStrategy struct {
10 | ed25519privKey ed25519.PrivateKey
11 | }
12 |
13 | func NewParsedEd25519KeySigningStrategy(ed25519privKey ed25519.PrivateKey) *ParsedEd25519KeySigningStrategy {
14 | return &ParsedEd25519KeySigningStrategy{
15 | ed25519privKey: ed25519privKey,
16 | }
17 | }
18 |
19 | func (bss ParsedEd25519KeySigningStrategy) Sign(data []byte) ([]byte, error) {
20 | return ed25519.Sign(bss.ed25519privKey, data), nil
21 | }
22 |
23 | func (bss ParsedEd25519KeySigningStrategy) GetPublicKey() (ed25519.PublicKey, error) {
24 | return bss.ed25519privKey.Public().(ed25519.PublicKey), nil
25 | }
26 |
--------------------------------------------------------------------------------
/go/integrityblock/signing-strategy-interface.go:
--------------------------------------------------------------------------------
1 | package integrityblock
2 |
3 | import (
4 | "crypto/ed25519"
5 | )
6 |
7 | type ISigningStrategy interface {
8 | Sign(data []byte) ([]byte, error)
9 | GetPublicKey() (ed25519.PublicKey, error)
10 |
11 | // TODO(sonkkeli): Implement once we have security approval.
12 | // IsDevSigning() bool
13 | }
14 |
--------------------------------------------------------------------------------
/go/integrityblock/testfile.wbn:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WICG/webpackage/5262e60be64301517a51577c81421d1422fb76fd/go/integrityblock/testfile.wbn
--------------------------------------------------------------------------------
/go/integrityblock/webbundleid/web-bundle-id.go:
--------------------------------------------------------------------------------
1 | package webbundleid
2 |
3 | import (
4 | "crypto/ed25519"
5 | "encoding/base32"
6 | "strings"
7 | )
8 |
9 | var webBundleIdSuffix = []byte{0x00, 0x01, 0x02}
10 |
11 | // GetWebBundleId returns a base32-encoded (without padding) ed25519 public key
12 | // combined with a 3-byte long suffix and transformed to lowercase. More information:
13 | // https://github.com/WICG/isolated-web-apps/blob/main/Scheme.md#signed-web-bundle-ids
14 | func GetWebBundleId(ed25519publicKey ed25519.PublicKey) string {
15 | keyWithSuffix := append([]byte(ed25519publicKey), webBundleIdSuffix...)
16 |
17 | // StdEncoding is the standard base32 encoding, as defined in RFC 4648.
18 | return strings.ToLower(base32.StdEncoding.EncodeToString(keyWithSuffix))
19 | }
20 |
--------------------------------------------------------------------------------
/go/integrityblock/webbundleid/web-bundle-id_test.go:
--------------------------------------------------------------------------------
1 | package webbundleid
2 |
3 | import (
4 | "crypto/ed25519"
5 | "testing"
6 |
7 | "github.com/WICG/webpackage/go/internal/signingalgorithm"
8 | )
9 |
10 | func TestGetWebBundleId(t *testing.T) {
11 | privateKeyString := "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIB8nP5PpWU7HiILHSfh5PYzb5GAcIfHZ+bw6tcd/LZXh\n-----END PRIVATE KEY-----"
12 | privateKey, err := signingalgorithm.ParsePrivateKey([]byte(privateKeyString))
13 | if err != nil {
14 | t.Errorf("integrityblock: Failed to parse the test private key. err: %v", err)
15 | }
16 |
17 | got := GetWebBundleId(privateKey.(ed25519.PrivateKey).Public().(ed25519.PublicKey))
18 | want := "4tkrnsmftl4ggvvdkfth3piainqragus2qbhf7rlz2a3wo3rh4wqaaic"
19 |
20 | if got != want {
21 | t.Errorf("integrityblock: got: %s\nwant: %s", got, want)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/go/internal/cbor/addinfo.go:
--------------------------------------------------------------------------------
1 | package cbor
2 |
3 | import "fmt"
4 |
5 | // 5 bits of the first byte on CBOR contain the additional information which can be either the
6 | // actual value or a value telling us how to get the value.
7 |
8 | // The meaning of additional information depends on the major type. For example, in major type 0,
9 | // the argument is the value of the data item itself (and in major type 1, the value of the data
10 | // item is computed from the argument); in major type 2 and 3, it gives the length of the string
11 | // data in bytes that follow; and in major types 4 and 5, it is used to determine the number of data
12 | // items enclosed.
13 |
14 | // https://www.rfc-editor.org/rfc/rfc8949.html#name-specification-of-the-cbor-e
15 |
16 | type AdditionalInfo int
17 |
18 | const (
19 | // The integers match with the length in bytes apart from reserved and indefinite.
20 | AdditionalInfoDirect AdditionalInfo = iota // 0->23
21 | AdditionalInfoOneByte // 24
22 | AdditionalInfoTwoBytes // 25
23 | AdditionalInfoFourBytes // 26
24 | AdditionalInfoEightBytes // 27
25 | AdditionalInfoReserved // 28->30
26 | AdditionalInfoIndefinite // 31
27 | )
28 |
29 | func convertToAdditionalInfo(b byte) AdditionalInfo {
30 | switch b & 0b00011111 {
31 | case 0x18:
32 | return AdditionalInfoOneByte
33 | case 0x19:
34 | return AdditionalInfoTwoBytes
35 | case 0x1A:
36 | return AdditionalInfoFourBytes
37 | case 0x1B:
38 | return AdditionalInfoEightBytes
39 | case 0x1C, 0x1D, 0x1E:
40 | return AdditionalInfoReserved
41 | case 0x1F:
42 | return AdditionalInfoIndefinite
43 | default:
44 | return AdditionalInfoDirect
45 | }
46 | }
47 |
48 | // getAdditionalInfoDirectValue is used for AdditionalInfoDirect to read the value following the additional info.
49 | func getAdditionalInfoDirectValue(b byte) byte {
50 | return b & 0b00011111
51 | }
52 |
53 | // getAdditionalInfoLength returns the length of the byte array following the additional info byte to read the unsigned integer from.
54 | func (ainfo AdditionalInfo) getAdditionalInfoLength() int {
55 | switch ainfo {
56 | case AdditionalInfoDirect:
57 | return 0
58 | case AdditionalInfoOneByte:
59 | return 1
60 | case AdditionalInfoTwoBytes:
61 | return 2
62 | case AdditionalInfoFourBytes:
63 | return 4
64 | case AdditionalInfoEightBytes:
65 | return 8
66 | default:
67 | panic("getAdditionalInfoLength() should never be called with: " + fmt.Sprint(ainfo))
68 | }
69 | }
70 |
71 | // getAdditionalInfoValueLowerLimit returns the unsigned integer limit which is the lowest that should
72 | // be using the AdditionalInfo in question. If the unsigned integer is smaller than the limit, it should
73 | // use less bytes than it is currently using meaning it is not following the deterministic principles.
74 | func (ainfo AdditionalInfo) getAdditionalInfoValueLowerLimit() uint64 {
75 | switch ainfo {
76 | case AdditionalInfoDirect:
77 | return 0
78 | case AdditionalInfoOneByte:
79 | // This is a special case and defined by the deterministic CBOR standard. Anything <= 23L
80 | // should be directly the value of the additional information.
81 | return 24
82 | case AdditionalInfoTwoBytes:
83 | return 256 // Aka 0b11111111 +1
84 | case AdditionalInfoFourBytes:
85 | return 1 << 16 // Aka 0b11111111_11111111 +1
86 | case AdditionalInfoEightBytes:
87 | return 1 << 32 // Aka 0b11111111_11111111_11111111_11111111 +1
88 | default:
89 | panic("Invalid additional information value: " + fmt.Sprint(ainfo))
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/go/internal/cbor/decoder.go:
--------------------------------------------------------------------------------
1 | package cbor
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "unicode/utf8"
8 | )
9 |
10 | type Decoder struct {
11 | r io.Reader
12 | }
13 |
14 | func NewDecoder(r io.Reader) *Decoder {
15 | return &Decoder{r}
16 | }
17 |
18 | func (d *Decoder) ReadByte() (byte, error) {
19 | b := make([]byte, 1)
20 | if _, err := io.ReadFull(d.r, b); err != nil {
21 | return 0, err
22 | }
23 | return b[0], nil
24 | }
25 |
26 | func (d *Decoder) decodeTypedUint() (Type, uint64, error) {
27 | const (
28 | maskType = 0xe0
29 | maskAdditionalInformation = 0x1f
30 | )
31 |
32 | b, err := d.ReadByte()
33 | if err != nil {
34 | return TypeOther, 0, err
35 | }
36 |
37 | t := Type(b & maskType)
38 | ai := b & maskAdditionalInformation
39 | nfollow := 0
40 | switch ai {
41 | case 24:
42 | nfollow = 1
43 | case 25:
44 | nfollow = 2
45 | case 26:
46 | nfollow = 4
47 | case 27:
48 | nfollow = 8
49 | default:
50 | nfollow = 0
51 | }
52 |
53 | n := uint64(0)
54 |
55 | var follow []byte
56 | if nfollow > 0 {
57 | follow = make([]byte, nfollow)
58 | if _, err := io.ReadFull(d.r, follow); err != nil {
59 | return t, 0, fmt.Errorf("cbor: Failed to read %d bytes following the tag byte: %v", nfollow, err)
60 | }
61 | for i := 0; i < nfollow; i++ {
62 | n = n<<8 | uint64(follow[i])
63 | }
64 | } else {
65 | n = uint64(ai)
66 | }
67 |
68 | return t, n, nil
69 | }
70 |
71 | func (d *Decoder) decodeOfType(expected Type) (uint64, error) {
72 | t, n, err := d.decodeTypedUint()
73 | if err != nil {
74 | return 0, err
75 | }
76 | if t != expected {
77 | return 0, fmt.Errorf("cbor: Expected type %v, got type %v", expected, t)
78 | }
79 | return n, nil
80 | }
81 |
82 | func (d *Decoder) DecodeUint() (uint64, error) {
83 | return d.decodeOfType(TypePosInt)
84 | }
85 |
86 | func (d *Decoder) DecodeArrayHeader() (uint64, error) {
87 | return d.decodeOfType(TypeArray)
88 | }
89 |
90 | func (d *Decoder) DecodeMapHeader() (uint64, error) {
91 | return d.decodeOfType(TypeMap)
92 | }
93 |
94 | func (d *Decoder) decodeBytesOfType(expected Type) ([]byte, error) {
95 | n, err := d.decodeOfType(expected)
96 | if err != nil {
97 | return nil, err
98 | }
99 | bs := new(bytes.Buffer)
100 | if _, err := io.CopyN(bs, d.r, int64(n)); err != nil {
101 | return nil, err
102 | }
103 | return bs.Bytes(), nil
104 | }
105 |
106 | func (d *Decoder) DecodeTextString() (string, error) {
107 | bs, err := d.decodeBytesOfType(TypeText)
108 | if err != nil {
109 | return "", err
110 | }
111 | if !utf8.Valid(bs) {
112 | return "", ErrInvalidUTF8
113 | }
114 | return string(bs), nil
115 | }
116 |
117 | func (d *Decoder) DecodeByteString() ([]byte, error) {
118 | return d.decodeBytesOfType(TypeBytes)
119 | }
120 |
--------------------------------------------------------------------------------
/go/internal/cbor/decoder_test.go:
--------------------------------------------------------------------------------
1 | package cbor_test
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 |
7 | . "github.com/WICG/webpackage/go/internal/cbor"
8 | )
9 |
10 | func TestDecodeByteString(t *testing.T) {
11 | var bytesTests = []struct {
12 | in []byte
13 | out []byte
14 | }{
15 | {
16 | in: []byte{0x40},
17 | out: []byte{},
18 | },
19 | {
20 | in: []byte{0x41, 0xab},
21 | out: []byte{0xab},
22 | },
23 | {
24 | in: []byte{
25 | 0x58, 0x19, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
26 | 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
27 | 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15,
28 | 0x16, 0x17, 0x18,
29 | },
30 | out: []byte{
31 | 0, 1, 2, 3, 4, 5, 6, 7,
32 | 8, 9, 10, 11, 12, 13, 14, 15,
33 | 16, 17, 18, 19, 20, 21, 22, 23,
34 | 24,
35 | },
36 | },
37 | }
38 |
39 | for _, test := range bytesTests {
40 | e := NewDecoder(bytes.NewReader(test.in))
41 | got, err := e.DecodeByteString()
42 | if err != nil {
43 | t.Errorf("Decode. err: %v", err)
44 | }
45 |
46 | if !bytes.Equal(test.out, got) {
47 | t.Errorf("%v expected to decode to %v, actual %v", test.in, test.out, got)
48 | }
49 | }
50 | }
51 |
52 | func TestDecodeByteStringNotCrashing(t *testing.T) {
53 | var in = []byte{0x5b, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
54 | e := NewDecoder(bytes.NewReader(in))
55 | _, err := e.DecodeByteString()
56 | if err == nil {
57 | t.Error("got success, want error")
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/go/internal/cbor/encoder_test.go:
--------------------------------------------------------------------------------
1 | package cbor_test
2 |
3 | import (
4 | "bytes"
5 | "encoding/hex"
6 | "strings"
7 | "testing"
8 |
9 | . "github.com/WICG/webpackage/go/internal/cbor"
10 | )
11 |
12 | // fromHex converts strings of the form "12 34 5678 9a" to byte slices.
13 | func fromHex(h string) []byte {
14 | bytes, err := hex.DecodeString(strings.Replace(h, " ", "", -1))
15 | if err != nil {
16 | panic(err)
17 | }
18 | return bytes
19 | }
20 |
21 | func TestEncodeInt(t *testing.T) {
22 | var inttests = []struct {
23 | i int64
24 | encoding string
25 | }{
26 | {0, "00"},
27 | {1, "01"},
28 | {10, "0a"},
29 | {23, "17"},
30 | {24, "1818"},
31 | {25, "1819"},
32 | {100, "1864"},
33 | {255, "18ff"},
34 | {256, "190100"},
35 | {1000, "1903e8"},
36 | {1000000, "1a000f4240"},
37 | {1000000000000, "1b000000e8d4a51000"},
38 | {-1, "20"},
39 | {-10, "29"},
40 | {-100, "3863"},
41 | {-1000, "3903e7"},
42 | {-9223372036854775808, "3b7fffffffffffffff"},
43 | }
44 | for _, test := range inttests {
45 | var b bytes.Buffer
46 | e := NewEncoder(&b)
47 |
48 | if err := e.EncodeInt(test.i); err != nil {
49 | t.Errorf("Encode. err: %v", err)
50 | }
51 | exp := fromHex(test.encoding)
52 |
53 | if !bytes.Equal(exp, b.Bytes()) {
54 | t.Errorf("%d expected to encode to %v, actual %v", test.i, exp, b.Bytes())
55 | }
56 | }
57 | }
58 |
59 | func TestEncodeByteString(t *testing.T) {
60 | var bytesTests = []struct {
61 | bs []byte
62 | encoding string
63 | }{
64 | {[]byte{}, "40"},
65 | {[]byte{0xab}, "41ab"},
66 | {
67 | []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24},
68 | "5819 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18",
69 | },
70 | }
71 | // This doesn't test every size of integer that might encode the length
72 | // of the byte string.
73 |
74 | for _, test := range bytesTests {
75 | var b bytes.Buffer
76 | e := NewEncoder(&b)
77 |
78 | if err := e.EncodeByteString(test.bs); err != nil {
79 | t.Errorf("Encode. err: %v", err)
80 | }
81 | exp := fromHex(test.encoding)
82 |
83 | if !bytes.Equal(exp, b.Bytes()) {
84 | t.Errorf("%v expected to encode to %v, actual %v", test.bs, exp, b.Bytes())
85 | }
86 | }
87 | }
88 |
89 | func TestEncodeTextString(t *testing.T) {
90 | var textTests = []struct {
91 | s string
92 | encoding string
93 | }{
94 | {"", "60"},
95 | {"a", "6161"},
96 | {"IETF", "6449455446"},
97 | {`"\`, "62225c"},
98 | {"\u00fc", "62c3bc"},
99 | {"\u6c34", "63e6b0b4"},
100 | {"\U00010151", "64f0908591"},
101 | }
102 | for _, test := range textTests {
103 | var b bytes.Buffer
104 | e := NewEncoder(&b)
105 |
106 | if err := e.EncodeTextString(test.s); err != nil {
107 | t.Errorf("Encode. err: %v", err)
108 | continue
109 | }
110 | exp := fromHex(test.encoding)
111 |
112 | if !bytes.Equal(exp, b.Bytes()) {
113 | t.Errorf("\"%s\" expected to encode to %v, actual %v", test.s, exp, b.Bytes())
114 | }
115 | }
116 |
117 | var b bytes.Buffer
118 | e := NewEncoder(&b)
119 | str := "\x80 <- invalid UTF-8"
120 | if err := e.EncodeTextString(str); err == nil {
121 | t.Errorf("Expected an error for malformed UTF-8 (%q)", str)
122 | }
123 | }
124 |
125 | func TestEncodeMapWithDuplicatedKeys(t *testing.T) {
126 | entries := []*MapEntryEncoder{
127 | GenerateMapEntry(func(keyE *Encoder, valueE *Encoder) {
128 | keyE.EncodeTextString("key")
129 | valueE.EncodeTextString("value1")
130 | }),
131 | GenerateMapEntry(func(keyE *Encoder, valueE *Encoder) {
132 | keyE.EncodeTextString("key")
133 | valueE.EncodeTextString("value2")
134 | }),
135 | }
136 |
137 | var b bytes.Buffer
138 | e := NewEncoder(&b)
139 | if err := e.EncodeMap(entries); err == nil {
140 | t.Error("Expected an error for duplicated map key")
141 | }
142 | }
143 |
144 | func TestMapEncoder(t *testing.T) {
145 | entries := []*MapEntryEncoder{
146 | GenerateMapEntry(func(keyE *Encoder, valueE *Encoder) {
147 | keyE.EncodeBool(false)
148 | valueE.EncodeTextString("false")
149 | }),
150 | GenerateMapEntry(func(keyE *Encoder, valueE *Encoder) {
151 | keyE.EncodeInt(-1)
152 | valueE.EncodeTextString("int -1")
153 | }),
154 | GenerateMapEntry(func(keyE *Encoder, valueE *Encoder) {
155 | keyE.EncodeInt(10)
156 | valueE.EncodeTextString("int 10")
157 | }),
158 | GenerateMapEntry(func(keyE *Encoder, valueE *Encoder) {
159 | keyE.EncodeInt(100)
160 | valueE.EncodeTextString("int 100")
161 | }),
162 | GenerateMapEntry(func(keyE *Encoder, valueE *Encoder) {
163 | keyE.EncodeTextString("aa")
164 | valueE.EncodeTextString("string \"aa\"")
165 | }),
166 | GenerateMapEntry(func(keyE *Encoder, valueE *Encoder) {
167 | keyE.EncodeTextString("z")
168 | valueE.EncodeTextString("string \"z\"")
169 | }),
170 | GenerateMapEntry(func(keyE *Encoder, valueE *Encoder) {
171 | keyE.EncodeArrayHeader(1)
172 | keyE.EncodeInt(-1)
173 | valueE.EncodeTextString("array [-1]")
174 | }),
175 | GenerateMapEntry(func(keyE *Encoder, valueE *Encoder) {
176 | keyE.EncodeArrayHeader(1)
177 | keyE.EncodeInt(100)
178 | valueE.EncodeTextString("array [100]")
179 | }),
180 | }
181 |
182 | // The keys in every map MUST be sorted in the bytewise lexicographic order of
183 | // their canonical encodings. For example, the following keys are correctly
184 | // sorted:
185 | // 1. 10, encoded as 0A.
186 | // 2. 100, encoded as 18 64.
187 | // 3. -1, encoded as 20.
188 | // 4. “z”, encoded as 61 7A.
189 | // 5. “aa”, encoded as 62 61 61.
190 | // 6. [100], encoded as 81 18 64.
191 | // 7. [-1], encoded as 81 20.
192 | // 8. false, encoded as F4.
193 | exp := fromHex(strings.Join([]string{
194 | "A8", // length
195 | "0A 66 69 6E 74 20 31 30",
196 | "18 64 67 69 6E 74",
197 | "20 31 30 30 20 66 69 6E 74 20 2D 31",
198 | "61 7A 6A 73 74 72 69 6E 67 20 22 7A 22",
199 | "62 61 61 6B 73 74 72 69 6E 67 20 22 61 61 22",
200 | "81 18 64 6B 61 72 72 61 79 20 5B 31 30 30 5D",
201 | "81 20 6A 61 72 72 61 79 20 5B 2D 31 5D",
202 | "F4 65 66 61 6C 73 65",
203 | }, ""))
204 |
205 | var b bytes.Buffer
206 | e := NewEncoder(&b)
207 | e.EncodeMap(entries)
208 | if !bytes.Equal(exp, b.Bytes()) {
209 | t.Errorf("the map expected to encode to %v, actual %v", exp, b.Bytes())
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/go/internal/cbor/types.go:
--------------------------------------------------------------------------------
1 | // Package cbor defines a parser and encoder for a subset of CBOR, RFC7049.
2 | package cbor
3 |
4 | type Type byte
5 |
6 | const (
7 | TypePosInt Type = 0
8 | TypeNegInt = 0x20
9 | TypeBytes = 0x40
10 | TypeText = 0x60
11 | TypeArray = 0x80
12 | TypeMap = 0xa0
13 | TypeTag = 0xc0
14 | TypeOther = 0xe0
15 | )
16 |
17 | // getMajorType returns the first 3 bits of the first byte representing cbor's major type.
18 | // https://www.rfc-editor.org/rfc/rfc8949.html#name-major-types
19 | func getMajorType(b byte) Type {
20 | return Type(b & 0b11100000)
21 | }
22 |
--------------------------------------------------------------------------------
/go/internal/signingalgorithm/certs.go:
--------------------------------------------------------------------------------
1 | package signingalgorithm
2 |
3 | import (
4 | "crypto"
5 | "crypto/ecdsa"
6 | "crypto/ed25519"
7 | "crypto/x509"
8 | "encoding/pem"
9 | "errors"
10 | "fmt"
11 | "os"
12 |
13 | "github.com/youmark/pkcs8"
14 | "golang.org/x/crypto/ssh/terminal"
15 | )
16 |
17 | func ParseCertificates(text []byte) ([]*x509.Certificate, error) {
18 | certs := []*x509.Certificate{}
19 | for len(text) > 0 {
20 | var block *pem.Block
21 | block, text = pem.Decode(text)
22 | if block == nil {
23 | break
24 | }
25 | if block.Type != "CERTIFICATE" {
26 | return nil, fmt.Errorf("signingalgorithm: found a block that contains %q.", block.Type)
27 | }
28 | if len(block.Headers) > 0 {
29 | return nil, fmt.Errorf("signingalgorithm: unexpected certificate headers: %v", block.Headers)
30 | }
31 | cert, err := x509.ParseCertificate(block.Bytes)
32 | if err != nil {
33 | return nil, err
34 | }
35 | certs = append(certs, cert)
36 | }
37 | return certs, nil
38 | }
39 |
40 | func ParsePrivateKey(text []byte) (crypto.PrivateKey, error) {
41 | for len(text) > 0 {
42 | var block *pem.Block
43 | block, text = pem.Decode(text)
44 | if block == nil {
45 | return nil, errors.New("signingalgorithm: invalid PEM block in private key.")
46 | }
47 |
48 | if block.Type == "ENCRYPTED PRIVATE KEY" {
49 | if privkey, err := parseEncryptedPrivateKeyBlock(block.Bytes); err == nil {
50 | return privkey, nil
51 | }
52 | } else {
53 | if privkey, err := parsePrivateKeyBlock(block.Bytes); err == nil {
54 | return privkey, nil
55 | }
56 | }
57 | }
58 |
59 | return nil, errors.New("signingalgorithm: could not find private key.")
60 | }
61 |
62 | func typeSupportedPKCS8key(keyInterface any) (crypto.PrivateKey, error) {
63 | switch typedKey := keyInterface.(type) {
64 | case *ecdsa.PrivateKey:
65 | return typedKey, nil
66 | case ed25519.PrivateKey:
67 | return typedKey, nil
68 | default:
69 | return nil, fmt.Errorf("signingalgorithm: unknown private key type in PKCS#8: %T", typedKey)
70 | }
71 | }
72 |
73 | func parsePrivateKeyBlock(derKey []byte) (crypto.PrivateKey, error) {
74 | // Try each of 2 key formats and take the first one that successfully parses.
75 | if key, err := x509.ParseECPrivateKey(derKey); err == nil {
76 | return key, nil
77 | }
78 |
79 | if keyInterface, err := x509.ParsePKCS8PrivateKey(derKey); err == nil {
80 | return typeSupportedPKCS8key(keyInterface)
81 | }
82 |
83 | return nil, errors.New("signingalgorithm: couldn't parse private key.")
84 | }
85 |
86 | // parseEncryptedPrivateKeyBlock reads the passphrase to decrypt an encrypted private key from either
87 | // WEB_BUNDLE_SIGNING_PASSPHRASE environment variable or if not set, it prompts a passphrase from the user.
88 | func parseEncryptedPrivateKeyBlock(derKey []byte) (crypto.PrivateKey, error) {
89 | passphrase := []byte(os.Getenv("WEB_BUNDLE_SIGNING_PASSPHRASE"))
90 |
91 | if len(passphrase) == 0 {
92 | fmt.Println("The key is passphrase-encrypted. Please provide the passphrase and then press ENTER. ")
93 | passphrase, _ = terminal.ReadPassword(0)
94 | if len(passphrase) == 0 {
95 | return nil, errors.New("signingalgorithm: invalid passphrase to decrypt the private key.")
96 | }
97 | } else {
98 | fmt.Println("The key is passphrase-encrypted. Passphrase was successfully read from WEB_BUNDLE_SIGNING_PASSPHRASE environment variable.")
99 | }
100 |
101 | if keyInterface, err := pkcs8.ParsePKCS8PrivateKey(derKey, passphrase); err == nil {
102 | return typeSupportedPKCS8key(keyInterface)
103 | }
104 |
105 | return nil, errors.New("signingalgorithm: couldn't parse encrypted private key.")
106 | }
107 |
108 | func ParsePublicKey(text []byte) (crypto.PublicKey, error) {
109 | for len(text) > 0 {
110 | var block *pem.Block
111 | block, text = pem.Decode(text)
112 | if block == nil {
113 | return nil, errors.New("signingalgorithm: invalid PEM block in public key.")
114 | }
115 |
116 | pubkey, err := parsePublicKeyBlock(block.Bytes)
117 | if err == nil {
118 | return pubkey, nil
119 | }
120 | }
121 | return nil, errors.New("signingalgorithm: could not find public key.")
122 | }
123 |
124 | // parsePublicKeyBlock parses any allowed type public key. Currently only allows parsing ed25519 type public keys.
125 | func parsePublicKeyBlock(derKey []byte) (crypto.PublicKey, error) {
126 | if keyInterface, err := x509.ParsePKIXPublicKey(derKey); err == nil {
127 | switch typedKey := keyInterface.(type) {
128 | case ed25519.PublicKey:
129 | return typedKey, nil
130 | default:
131 | return nil, fmt.Errorf("signingalgorithm: unknown public key type: %T", typedKey)
132 | }
133 | }
134 | return nil, errors.New("signingalgorithm: couldn't parse public key.")
135 | }
136 |
--------------------------------------------------------------------------------
/go/internal/signingalgorithm/signingalgorithm.go:
--------------------------------------------------------------------------------
1 | package signingalgorithm
2 |
3 | import (
4 | "crypto"
5 | "crypto/ecdsa"
6 | "crypto/elliptic"
7 | "crypto/sha256"
8 | _ "crypto/sha512"
9 | "encoding/asn1"
10 | "errors"
11 | "fmt"
12 | "io"
13 | "math/big"
14 | )
15 |
16 | type SigningAlgorithm interface {
17 | Sign(m []byte) ([]byte, error)
18 | }
19 |
20 | type MockSigningAlgorithm struct{}
21 |
22 | // Sign returns the SHA-256 hash of the input.
23 | func (e *MockSigningAlgorithm) Sign(m []byte) ([]byte, error) {
24 | h := sha256.Sum256(m)
25 | return h[:], nil
26 | }
27 |
28 | type ecdsaSigningAlgorithm struct {
29 | privKey *ecdsa.PrivateKey
30 | hash crypto.Hash
31 | rand io.Reader
32 | }
33 |
34 | // Ecdsa-Sig-Value structure in Section 2.2.3 of RFC 3279.
35 | type ecdsaSigValue struct {
36 | R, S *big.Int
37 | }
38 |
39 | func (e *ecdsaSigningAlgorithm) Sign(m []byte) ([]byte, error) {
40 | hash := e.hash.New()
41 | hash.Write(m)
42 | r, s, err := ecdsa.Sign(e.rand, e.privKey, hash.Sum(nil))
43 | if err != nil {
44 | return nil, err
45 | }
46 | return asn1.Marshal(ecdsaSigValue{r, s})
47 | }
48 |
49 | func SigningAlgorithmForPrivateKey(pk crypto.PrivateKey, rand io.Reader) (SigningAlgorithm, error) {
50 | switch pk := pk.(type) {
51 | case *ecdsa.PrivateKey:
52 | switch name := pk.Curve.Params().Name; name {
53 | case elliptic.P256().Params().Name:
54 | return &ecdsaSigningAlgorithm{pk, crypto.SHA256, rand}, nil
55 | case elliptic.P384().Params().Name:
56 | return &ecdsaSigningAlgorithm{pk, crypto.SHA384, rand}, nil
57 | default:
58 | return nil, fmt.Errorf("signingalgorithm: unknown ECDSA curve: %s", name)
59 | }
60 | }
61 | return nil, fmt.Errorf("signingalgorithm: unknown private key type: %T", pk)
62 | }
63 |
64 | type Verifier interface {
65 | Verify(msg, sig []byte) (bool, error)
66 | }
67 |
68 | type ecdsaVerifier struct {
69 | pubKey *ecdsa.PublicKey
70 | hash crypto.Hash
71 | }
72 |
73 | func (e *ecdsaVerifier) Verify(msg, sig []byte) (bool, error) {
74 | var v ecdsaSigValue
75 | rest, err := asn1.Unmarshal(sig, &v)
76 | if err != nil {
77 | return false, fmt.Errorf("signingalgorithm: failed to ASN.1 decode the signature: %v", err)
78 | }
79 | if len(rest) > 0 {
80 | return false, errors.New("signingalgorithm: extra data at the signature end")
81 | }
82 |
83 | hash := e.hash.New()
84 | hash.Write(msg)
85 | return ecdsa.Verify(e.pubKey, hash.Sum(nil), v.R, v.S), nil
86 | }
87 |
88 | func VerifierForPublicKey(k crypto.PublicKey) (Verifier, error) {
89 | switch k := k.(type) {
90 | case *ecdsa.PublicKey:
91 | switch name := k.Params().Name; name {
92 | case elliptic.P256().Params().Name:
93 | return &ecdsaVerifier{k, crypto.SHA256}, nil
94 | case elliptic.P384().Params().Name:
95 | return &ecdsaVerifier{k, crypto.SHA384}, nil
96 | default:
97 | return nil, fmt.Errorf("signingalgorithm: unknown ECDSA curve: %s", name)
98 | }
99 | }
100 | return nil, fmt.Errorf("signingalgorithm: unknown public key type: %T", k)
101 | }
102 |
--------------------------------------------------------------------------------
/go/internal/signingalgorithm/signingalgorithm_test.go:
--------------------------------------------------------------------------------
1 | package signingalgorithm_test
2 |
3 | import (
4 | "crypto/ecdsa"
5 | "crypto/ed25519"
6 | "crypto/elliptic"
7 | "crypto/rand"
8 | "os"
9 | "testing"
10 |
11 | . "github.com/WICG/webpackage/go/internal/signingalgorithm"
12 | )
13 |
14 | func TestSignVerify_ECDSA_P256_SHA256(t *testing.T) {
15 | pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
16 | if err != nil {
17 | t.Fatalf("Failed to generate ecdsa private key: %v", err)
18 | return
19 | }
20 |
21 | alg, err := SigningAlgorithmForPrivateKey(pk, rand.Reader)
22 | if err != nil {
23 | t.Fatalf("Failed to pick signing algorithm for ecdsa private key: %v", err)
24 | return
25 | }
26 |
27 | msg := []byte("foobar")
28 | sig, err := alg.Sign(msg)
29 | if err != nil {
30 | t.Fatalf("Failed to sign: %v", err)
31 | return
32 | }
33 |
34 | verifier, err := VerifierForPublicKey(pk.Public())
35 | if err != nil {
36 | t.Fatalf("Failed to pick verifier for ecdsa public key: %v", err)
37 | return
38 | }
39 |
40 | ok, err := verifier.Verify(msg, sig)
41 | if err != nil {
42 | t.Errorf("Verification failed: %v", err)
43 | }
44 | if !ok {
45 | t.Error("Unexpected verification failure")
46 | }
47 |
48 | msg[0] = 'q'
49 | ok, err = verifier.Verify(msg, sig)
50 | if err != nil {
51 | t.Errorf("Verification failed: %v", err)
52 | }
53 | if ok {
54 | t.Error("Unexpected verification success")
55 | }
56 | }
57 |
58 | func TestSignVerify_ED25519(t *testing.T) {
59 | privateKeyString := "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIB8nP5PpWU7HiILHSfh5PYzb5GAcIfHZ+bw6tcd/LZXh\n-----END PRIVATE KEY-----"
60 |
61 | pk, err := ParsePrivateKey([]byte(privateKeyString))
62 | if err != nil {
63 | t.Errorf("integrityblock: Failed to parse the test private key. err: %v", err)
64 | }
65 |
66 | ed25519pri := pk.(ed25519.PrivateKey)
67 | ed25519pub := ed25519pri.Public().(ed25519.PublicKey)
68 |
69 | msg := []byte("foobar")
70 | signature := ed25519.Sign(ed25519pri, msg)
71 |
72 | if !ed25519.Verify(ed25519pub, msg, signature) {
73 | t.Error("Signature verification failed with unencrypted Ed25519 key")
74 | }
75 | }
76 |
77 | func TestSignVerify_ED25519_Encrypted(t *testing.T) {
78 | encryptedPrivateKeyString := "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIGbMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAhOw2E7LxOkzQICCAAw\nDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEJZqH2axMFEvdFmJLZlnch4EQMfJ\nAa/4uAmWqu2N5aOn2yIz3Ri+vQ/rzBPrvIaoDxYUUxwujJFujSbr3lnagHlOPptU\n7XhjbPbeOqidLqyv5rA=\n-----END ENCRYPTED PRIVATE KEY-----"
79 |
80 | os.Setenv("WEB_BUNDLE_SIGNING_PASSPHRASE", "helloworld" /*=passphrase*/)
81 |
82 | pk, err := ParsePrivateKey([]byte(encryptedPrivateKeyString))
83 | if err != nil {
84 | t.Errorf("integrityblock: Failed to parse the test private key. err: %v", err)
85 | }
86 |
87 | ed25519pri := pk.(ed25519.PrivateKey)
88 | ed25519pub := ed25519pri.Public().(ed25519.PublicKey)
89 |
90 | msg := []byte("foobar")
91 | signature := ed25519.Sign(ed25519pri, msg)
92 |
93 | if !ed25519.Verify(ed25519pub, msg, signature) {
94 | t.Error("Signature verification failed with encrypted Ed25519 key")
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/go/internal/testhelper/cbor.go:
--------------------------------------------------------------------------------
1 | package testhelper
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "sort"
7 | "strings"
8 |
9 | "github.com/ugorji/go/codec"
10 | )
11 |
12 | // readableString converts an arbitrary value v to a string.
13 | //
14 | // readableString does a basically same thing as fmt.Sprintf("%q", v), but
15 | // the difference is that map keys are ordered in alphabetical order so that
16 | // the results are deterministic.
17 | func readableString(v interface{}) string {
18 | switch v := v.(type) {
19 | case []interface{}:
20 | vals := []string{}
21 | for _, val := range v {
22 | vals = append(vals, readableString(val))
23 | }
24 | return "[" + strings.Join(vals, " ") + "]"
25 | case map[interface{}]interface{}:
26 | keys := []string{}
27 | // Assume that keys are strings.
28 | for k := range v {
29 | keys = append(keys, k.(string))
30 | }
31 | sort.Strings(keys)
32 | vals := []string{}
33 | for _, k := range keys {
34 | val := v[k]
35 | vals = append(vals, fmt.Sprintf("%q:", k)+readableString(val))
36 | }
37 | return "map[" + strings.Join(vals, " ") + "]"
38 | case string, []byte:
39 | return fmt.Sprintf("%q", v)
40 | case uint64:
41 | return fmt.Sprintf("%d", v)
42 | default:
43 | panic(fmt.Sprintf("not supported type: %T", v))
44 | }
45 | }
46 |
47 | // CborBinaryToReadableString converts a CBOR binary to a readable string.
48 | func CborBinaryToReadableString(b []byte) (string, error) {
49 | r := bytes.NewReader(b)
50 |
51 | var decoded interface{}
52 | handle := &codec.CborHandle{}
53 | if err := codec.NewDecoder(r, handle).Decode(&decoded); err != nil {
54 | return "", err
55 | }
56 | return readableString(decoded), nil
57 | }
58 |
--------------------------------------------------------------------------------
/go/signedexchange/certurl/certchain-expected.cbor:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WICG/webpackage/5262e60be64301517a51577c81421d1422fb76fd/go/signedexchange/certurl/certchain-expected.cbor
--------------------------------------------------------------------------------
/go/signedexchange/certurl/certchain_test.go:
--------------------------------------------------------------------------------
1 | package certurl_test
2 |
3 | import (
4 | "bytes"
5 | "io/ioutil"
6 | "testing"
7 |
8 | "github.com/WICG/webpackage/go/internal/signingalgorithm"
9 | "github.com/WICG/webpackage/go/internal/testhelper"
10 | . "github.com/WICG/webpackage/go/signedexchange/certurl"
11 | )
12 |
13 | func createCertChain(t *testing.T) CertChain {
14 | in, err := ioutil.ReadFile("test-cert.pem")
15 | if err != nil {
16 | t.Fatalf("Cannot read test-cert.pem: %v", err)
17 | }
18 | certs, err := signingalgorithm.ParseCertificates(in)
19 | if err != nil {
20 | t.Fatalf("Cannot parse test-cert.pem: %v", err)
21 | }
22 | chain, err := NewCertChain(certs, []byte("OCSP"), []byte("SCT"))
23 | if err != nil {
24 | t.Fatalf("NewCertChain failed: %v", err)
25 | }
26 | return chain
27 | }
28 |
29 | func TestParsePEM(t *testing.T) {
30 | expected, err := ioutil.ReadFile("certchain-expected.cbor")
31 | if err != nil {
32 | t.Fatalf("Cannot read certchain-expected.cbor: %v", err)
33 | }
34 |
35 | certChain := createCertChain(t)
36 |
37 | buf := &bytes.Buffer{}
38 | if err := certChain.Write(buf); err != nil {
39 | t.Fatal(err)
40 | }
41 | if !bytes.Equal(buf.Bytes(), expected) {
42 | got, err := testhelper.CborBinaryToReadableString(buf.Bytes())
43 | if err != nil {
44 | t.Fatal(err)
45 | }
46 | want, err := testhelper.CborBinaryToReadableString(expected)
47 | if err != nil {
48 | t.Fatal(err)
49 | }
50 | t.Errorf("CertificateMessageFromPEM:\ngot: %q,\nwant: %q", got, want)
51 | }
52 | }
53 |
54 | func TestRoundtrip(t *testing.T) {
55 | original := createCertChain(t)
56 | buf := &bytes.Buffer{}
57 | if err := original.Write(buf); err != nil {
58 | t.Fatal(err)
59 | }
60 |
61 | parsed, err := ReadCertChain(buf)
62 | if err != nil {
63 | t.Fatal(err)
64 | }
65 |
66 | if len(original) != len(parsed) {
67 | t.Fatalf("Cert chain length differs: want %d, got %d", len(original), len(parsed))
68 | }
69 | for i := 0; i < len(original); i++ {
70 | want := original[i]
71 | got := parsed[i]
72 | if !bytes.Equal(want.Cert.Raw, got.Cert.Raw) {
73 | t.Errorf("Cert at position %d differs:\n want: %v\n got: %v", i, want.Cert.Raw, got.Cert.Raw)
74 | }
75 | if !bytes.Equal(want.OCSPResponse, got.OCSPResponse) {
76 | t.Errorf("OCSP at position %d differs:\n want: %v\n got: %v", i, want.OCSPResponse, got.OCSPResponse)
77 | }
78 | if !bytes.Equal(want.SCTList, got.SCTList) {
79 | t.Errorf("SCT at position %d differs:\n want: %v\n got: %v", i, want.SCTList, got.SCTList)
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/go/signedexchange/certurl/ocsp.go:
--------------------------------------------------------------------------------
1 | package certurl
2 |
3 | import (
4 | "bytes"
5 | "crypto/x509"
6 | "encoding/base64"
7 | "fmt"
8 | "golang.org/x/crypto/ocsp"
9 | "io"
10 | "io/ioutil"
11 | "net/http"
12 | "net/url"
13 | )
14 |
15 | func CreateOCSPRequest(certs []*x509.Certificate, preferGET bool) (*http.Request, error) {
16 | if len(certs) < 2 {
17 | return nil, fmt.Errorf("Could not fetch OCSP response: Issuer certificate not found")
18 | }
19 | cert := certs[0]
20 | if len(cert.OCSPServer) == 0 {
21 | return nil, fmt.Errorf("Could not fetch OCSP response: No OCSP responder field")
22 | }
23 | ocspURL := cert.OCSPServer[0]
24 | issuer := certs[1]
25 |
26 | ocspRequest, err := ocsp.CreateRequest(cert, issuer, &ocsp.RequestOptions{})
27 | if err != nil {
28 | return nil, err
29 | }
30 |
31 | // Use GET for small requests per https://tools.ietf.org/html/rfc5019#section-5.
32 |
33 | // Use QueryEscape here instead of PathEscape to escape not only '/' but also '+' and '='
34 | // to align with the exmaple in https://tools.ietf.org/html/rfc5019#section-5.
35 | // Note that the string to be escaped doesn't contain other symbols as it is base64 encoded.
36 | getURL := ocspURL + "/" + url.QueryEscape(base64.StdEncoding.EncodeToString(ocspRequest))
37 | if preferGET && len(getURL) <= 255 {
38 | request, err := http.NewRequest("GET", getURL, nil)
39 | if err != nil {
40 | return nil, err
41 | }
42 | request.Header.Add("Accept", "application/ocsp-response")
43 | return request, nil
44 | } else {
45 | request, err := http.NewRequest("POST", ocspURL, bytes.NewReader(ocspRequest))
46 | if err != nil {
47 | return nil, err
48 | }
49 | request.Header.Add("Content-Type", "application/ocsp-request")
50 | request.Header.Add("Accept", "application/ocsp-response")
51 | return request, nil
52 | }
53 | }
54 |
55 | func FetchOCSPResponse(certs []*x509.Certificate, preferGET bool) ([]byte, error) {
56 | request, err := CreateOCSPRequest(certs, preferGET)
57 | if err != nil {
58 | return nil, err
59 | }
60 |
61 | client := &http.Client{}
62 | response, err := client.Do(request)
63 | if err != nil {
64 | return nil, err
65 | }
66 | defer response.Body.Close()
67 | output, err := ioutil.ReadAll(response.Body)
68 | if err != nil {
69 | return nil, err
70 | }
71 |
72 | return output, nil
73 | }
74 |
75 | func (chain CertChain) prettyPrintOCSP(w io.Writer, OCSPResponse []byte) {
76 | var issuer *x509.Certificate
77 | if len(chain) >= 2 {
78 | issuer = chain[1].Cert
79 | }
80 | o, err := ocsp.ParseResponseForCert(OCSPResponse, chain[0].Cert, issuer)
81 | if err != nil {
82 | fmt.Fprintln(w, "Error: Invalid OCSP response:", err)
83 | return
84 | }
85 | ocspStatusToString := map[int]string{
86 | ocsp.Good: "good",
87 | ocsp.Revoked: "revoked",
88 | ocsp.Unknown: "unknown",
89 | }
90 | fmt.Fprintf(w, " Status: %d (%s)\n", o.Status, ocspStatusToString[o.Status])
91 | fmt.Fprintln(w, " ProducedAt:", o.ProducedAt)
92 | fmt.Fprintln(w, " ThisUpdate:", o.ThisUpdate)
93 | fmt.Fprintln(w, " NextUpdate:", o.NextUpdate)
94 |
95 | prettyPrintSCTFromOCSP(w, o)
96 | }
97 |
--------------------------------------------------------------------------------
/go/signedexchange/certurl/ocsp_test.go:
--------------------------------------------------------------------------------
1 | package certurl_test
2 |
3 | import (
4 | "io/ioutil"
5 | "testing"
6 |
7 | "github.com/WICG/webpackage/go/internal/signingalgorithm"
8 | . "github.com/WICG/webpackage/go/signedexchange/certurl"
9 | "golang.org/x/crypto/ocsp"
10 | )
11 |
12 | func TestCreateOCSPRequestSmall(t *testing.T) {
13 | expectedRequestURL := "http://ocsp.digicert.com/MFEwTzBNMEswSTAJBgUrDgMCGgUABBTPJvUY%2Bsl%2Bj4yzQuAcL2oQno5fCgQUUWj%2FkK8CB3U8zNllZGKiErhZcjsCEA5kxfvCNq3hSxcq60HHjLA%3D"
14 |
15 | pem, err := ioutil.ReadFile("test-cert.pem")
16 | if err != nil {
17 | t.Fatalf("Cannot read test-cert.pem: %v", err)
18 | }
19 | certs, err := signingalgorithm.ParseCertificates(pem)
20 | if err != nil {
21 | t.Fatalf("Cannot parse test-cert.pem: %v", err)
22 | }
23 |
24 | req, err := CreateOCSPRequest(certs, true)
25 | if err != nil {
26 | t.Fatalf("CreateOCSPRequest failed: %v", err)
27 | }
28 |
29 | if req.Method != "GET" {
30 | t.Errorf("OCSP request Method:\ngot: %q,\nwant: %q", req.Method, "GET")
31 | }
32 | if req.URL.String() != expectedRequestURL {
33 | t.Errorf("OCSP request URL:\ngot: %q,\nwant: %q", req.URL.String(), expectedRequestURL)
34 | }
35 | if req.Header.Get("Accept") != "application/ocsp-response" {
36 | t.Errorf("OCSP request Accept header:\ngot: %q,\nwant: %q", req.Header.Get("Accept"), "application/ocsp-response")
37 | }
38 | }
39 |
40 | func TestCreateOCSPRequestLarge(t *testing.T) {
41 | // $ openssl genrsa -out ca.key 2048
42 | // $ openssl req -x509 -new -nodes -sha256 -key ca.key -out ca.pem -subj '/CN=example.com/O=Test/C=US'
43 | // $ openssl ecparam -out test-cert-long.key -name prime256v1 -genkey
44 | // $ openssl req -new -sha256 -key test-cert-long.key -out test-cert-long.csr -subj /CN=example.com
45 | // $ openssl x509 -req -in test-cert-long.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out test-cert-long-leaf.pem \
46 | // -extfile <(echo -e "1.3.6.1.4.1.11129.2.1.22 = ASN1:NULL\nsubjectAltName=DNS:example.com\nauthorityInfoAccess=OCSP;URI:http://very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-long-ocsp.example.com")
47 | // $ openssl test-cert-long-leaf.pem ca.pem > test-cert-long.pem
48 |
49 | expectedResponderURL := "http://very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-long-ocsp.example.com"
50 |
51 | pem, err := ioutil.ReadFile("test-cert-long.pem")
52 | if err != nil {
53 | t.Fatalf("Cannot read test-cert-long.pem: %v", err)
54 | }
55 | certs, err := signingalgorithm.ParseCertificates(pem)
56 | if err != nil {
57 | t.Fatalf("Cannot parse test-cert-long.pem: %v", err)
58 | }
59 |
60 | req, err := CreateOCSPRequest(certs, true)
61 | if err != nil {
62 | t.Fatalf("CreateOCSPRequest failed: %v", err)
63 | }
64 |
65 | if req.Method != "POST" {
66 | t.Errorf("OCSP request Method:\ngot: %q,\nwant: %q", req.Method, "POST")
67 | }
68 | if req.URL.String() != expectedResponderURL {
69 | t.Errorf("OCSP request URL:\ngot: %q,\nwant: %q", req.URL.String(), expectedResponderURL)
70 | }
71 | if req.Header.Get("Content-Type") != "application/ocsp-request" {
72 | t.Errorf("OCSP request Content-Type header:\ngot: %q,\nwant: %q", req.Header.Get("Content-Type"), "application/ocsp-request")
73 | }
74 | if req.Header.Get("Accept") != "application/ocsp-response" {
75 | t.Errorf("OCSP request Accept header:\ngot: %q,\nwant: %q", req.Header.Get("Accept"), "application/ocsp-response")
76 | }
77 |
78 | if req.Body == nil {
79 | t.Fatalf("Empty request body")
80 | }
81 | defer req.Body.Close()
82 | body, err := ioutil.ReadAll(req.Body)
83 | if err != nil {
84 | t.Fatalf("Cannot read request body: %v", err)
85 | }
86 | if _, err = ocsp.ParseRequest(body); err != nil {
87 | t.Errorf("Cannot parse request body as an OCSP request: %v", err)
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/go/signedexchange/certurl/sct.go:
--------------------------------------------------------------------------------
1 | package certurl
2 |
3 | import (
4 | "bytes"
5 | "crypto/x509"
6 | "crypto/x509/pkix"
7 | "encoding/asn1"
8 | "encoding/base64"
9 | "encoding/binary"
10 | "fmt"
11 | "golang.org/x/crypto/ocsp"
12 | "io"
13 | )
14 |
15 | const maxSerializedSCTLength = 0xffff
16 |
17 | var (
18 | // OIDs for embedded SCTs (Section 3.3 of RFC6962).
19 | oidCertExtension = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 2}
20 | oidOCSPExtension = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 5}
21 | )
22 |
23 | // SerializeSCTList serializes a list of SignedCertificateTimestamps into a
24 | // SignedCertificateTimestampList (RFC6962 Section 3.3).
25 | func SerializeSCTList(scts [][]byte) ([]byte, error) {
26 | total_length := 0
27 | for _, sct := range scts {
28 | if len(sct) > maxSerializedSCTLength {
29 | return nil, fmt.Errorf("SCT too large")
30 | }
31 | total_length += len(sct) + 2 // +2 for length
32 | }
33 | if total_length > maxSerializedSCTLength {
34 | return nil, fmt.Errorf("SCT list too large")
35 | }
36 |
37 | var buf bytes.Buffer
38 | if err := binary.Write(&buf, binary.BigEndian, uint16(total_length)); err != nil {
39 | return nil, err
40 | }
41 | for _, sct := range scts {
42 | if err := binary.Write(&buf, binary.BigEndian, uint16(len(sct))); err != nil {
43 | return nil, err
44 | }
45 | if _, err := buf.Write(sct); err != nil {
46 | return nil, err
47 | }
48 | }
49 | return buf.Bytes(), nil
50 | }
51 |
52 | // HasEmbeddedSCT returns true if the certificate or the OCSP response have
53 | // embedded SCT list.
54 | func HasEmbeddedSCT(cert *x509.Certificate, ocsp_resp *ocsp.Response) bool {
55 | return (cert != nil && findExtensionWithOID(cert.Extensions, oidCertExtension) != nil) ||
56 | (ocsp_resp != nil && findExtensionWithOID(ocsp_resp.Extensions, oidOCSPExtension) != nil)
57 | }
58 |
59 | func findExtensionWithOID(extensions []pkix.Extension, oid asn1.ObjectIdentifier) *pkix.Extension {
60 | for _, ext := range extensions {
61 | if ext.Id.Equal(oid) {
62 | return &ext
63 | }
64 | }
65 | return nil
66 | }
67 |
68 | func prettyPrintSCTFromCert(w io.Writer, cert *x509.Certificate) {
69 | prettyPrintSCTExtension(w, cert.Extensions, oidCertExtension)
70 | }
71 |
72 | func prettyPrintSCTFromOCSP(w io.Writer, ocspResp *ocsp.Response) {
73 | prettyPrintSCTExtension(w, ocspResp.Extensions, oidOCSPExtension)
74 | }
75 |
76 | func prettyPrintSCTExtension(w io.Writer, extensions []pkix.Extension, oid asn1.ObjectIdentifier) {
77 | ext := findExtensionWithOID(extensions, oid)
78 | if ext == nil {
79 | return
80 | }
81 | var sct []byte
82 | if _, err := asn1.Unmarshal(ext.Value, &sct); err != nil {
83 | fmt.Fprintln(w, "Error: Cannot parse SCT extension as ASN.1 OCTET STRING:", err)
84 | return
85 | }
86 | fmt.Fprintln(w, " Embedded SCT:")
87 | prettyPrintSCT(w, sct)
88 | }
89 |
90 | func prettyPrintSCT(w io.Writer, SCTList []byte) {
91 | buf := bytes.NewBuffer(SCTList)
92 |
93 | var total_length uint16
94 | if err := binary.Read(buf, binary.BigEndian, &total_length); err != nil {
95 | fmt.Fprintln(w, "Error: Cannot parse length of SignedCertificateTimestampList:", err)
96 | return
97 | }
98 | if int(total_length) != buf.Len() {
99 | fmt.Fprintf(w, "Error: Unexpected length of SignedCertificateTimestampList. expected: %d, actual: %d\n", total_length, buf.Len())
100 | return
101 | }
102 |
103 | for buf.Len() > 0 {
104 | var length uint16
105 | if err := binary.Read(buf, binary.BigEndian, &length); err != nil {
106 | fmt.Fprintln(w, "Error: Cannot parse length of SerializedSCT:", err)
107 | return
108 | }
109 | sct := buf.Next(int(length))
110 | if int(length) != len(sct) {
111 | fmt.Fprintf(w, "Error: Unexpected length of SerializedSCT. expected: %d, actual: %d\n", length, len(sct))
112 | return
113 | }
114 |
115 | // sct[0] is the Version and sct[1:33] is the LogID of the SCT (Section 3.2 of RFC6962).
116 | if len(sct) < 33 {
117 | fmt.Fprintf(w, "Error: SCT too short (%d bytes)\n", len(sct))
118 | return
119 | }
120 | if sct[0] != 0 {
121 | fmt.Fprintf(w, "Error: Unknown version of SCT (%d)\n", sct[0])
122 | return
123 | }
124 | fmt.Fprintln(w, " LogID:", base64.StdEncoding.EncodeToString(sct[1:33]))
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/go/signedexchange/certurl/sct_test.go:
--------------------------------------------------------------------------------
1 | package certurl_test
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 |
7 | . "github.com/WICG/webpackage/go/signedexchange/certurl"
8 | )
9 |
10 | func TestSerializeSCTList(t *testing.T) {
11 | expected := []byte{
12 | 0x00, 0x0a, // length
13 | 0x00, 0x03, 0x01, 0x02, 0x03, // length + first SCT
14 | 0x00, 0x03, 0x04, 0x05, 0x06, // length + second SCT
15 | }
16 | serialized, err := SerializeSCTList([][]byte{{1, 2, 3}, {4, 5, 6}})
17 | if err != nil {
18 | t.Fatalf("SerializeSCTList failed: %v", err)
19 | }
20 | if !bytes.Equal(expected, serialized) {
21 | t.Errorf("The SCTs expected to serialize to %v, actual %v", expected, serialized)
22 | }
23 | }
24 |
25 | func TestSerializeSCTListTooLarge(t *testing.T) {
26 | _, err := SerializeSCTList([][]byte{make([]byte, 65536)})
27 | if err == nil {
28 | t.Errorf("SerializeSCTList didn't fail with too large SCT")
29 | }
30 |
31 | // (32766 + 2) * 2 = 65536
32 | _, err = SerializeSCTList([][]byte{make([]byte, 32766), make([]byte, 32766)})
33 | if err == nil {
34 | t.Errorf("SerializeSCTList didn't fail with too large SCT list")
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/go/signedexchange/certurl/test-cert-long.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDXzCCAkegAwIBAgIUG1qnb/SsH0FnijEAPJAsnkld7gUwDQYJKoZIhvcNAQEL
3 | BQAwMjEUMBIGA1UEAwwLZXhhbXBsZS5jb20xDTALBgNVBAoMBFRlc3QxCzAJBgNV
4 | BAYTAlVTMB4XDTE5MTIxNjA2MDIwM1oXDTIwMDExNTA2MDIwM1owFjEUMBIGA1UE
5 | AwwLZXhhbXBsZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAReufeIWbn/
6 | T2PPCpBP6Vq7MtAUn6Td/nBMihmbk+x32AToU4Yl1zIF69Qxp217wEGQC2XILzJC
7 | 5K9AH2TGDGRqo4IBUjCCAU4wEAYKKwYBBAHWeQIBFgQCBQAwFgYDVR0RBA8wDYIL
8 | ZXhhbXBsZS5jb20wggEgBggrBgEFBQcBAQSCARIwggEOMIIBCgYIKwYBBQUHMAGG
9 | gf1odHRwOi8vdmVyeS12ZXJ5LXZlcnktdmVyeS12ZXJ5LXZlcnktdmVyeS12ZXJ5
10 | LXZlcnktdmVyeS12ZXJ5LXZlcnktdmVyeS12ZXJ5LXZlcnktdmVyeS12ZXJ5LXZl
11 | cnktdmVyeS12ZXJ5LXZlcnktdmVyeS12ZXJ5LXZlcnktdmVyeS12ZXJ5LXZlcnkt
12 | dmVyeS12ZXJ5LXZlcnktdmVyeS12ZXJ5LXZlcnktdmVyeS12ZXJ5LXZlcnktdmVy
13 | eS12ZXJ5LXZlcnktdmVyeS12ZXJ5LXZlcnktdmVyeS12ZXJ5LXZlcnktbG9uZy1v
14 | Y3NwLmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQDDEvvRu1Wwm66W+/Gv
15 | yn/OJesqWIHkvoEvwoqVzJQpTrhWVtM0+MPOcXfouTvTGk6OSmjg9UBrtIUGIvUy
16 | NTYFzRSmhASIreW7ypFQ/38of+++btlTil5usrACvJcocEU6Q0DkzBOyKWU33yqf
17 | Qbx5bTIFRLZqQ6YL+z6WnzoPWXeyOu+8TXX+VIC4To0s7WgJjuQaYMR/R27CD+hv
18 | sEQDoFlnWdE+U+PVje3U3IuCZ3gQiaabbMIiedDPC7E37cUAkJE/qcsMXGcfmtwK
19 | Pj7ObXEfSTU+NI0fqzm5mGXIlCUwX7Y2Oa3SBgeWKvXbzR4e1e12uWhJw2QFx8gn
20 | izuq
21 | -----END CERTIFICATE-----
22 | -----BEGIN CERTIFICATE-----
23 | MIIDRTCCAi2gAwIBAgIUXb3MLl0EOmPNxEazF7FukiKzh4swDQYJKoZIhvcNAQEL
24 | BQAwMjEUMBIGA1UEAwwLZXhhbXBsZS5jb20xDTALBgNVBAoMBFRlc3QxCzAJBgNV
25 | BAYTAlVTMB4XDTE5MTIxNjA1MzIyNVoXDTIwMDExNTA1MzIyNVowMjEUMBIGA1UE
26 | AwwLZXhhbXBsZS5jb20xDTALBgNVBAoMBFRlc3QxCzAJBgNVBAYTAlVTMIIBIjAN
27 | BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyu7CvrmgL/RFaR+uQLzojxpmujLL
28 | sjyZViPa749gZdl8K2os1vb2C4KuAqIcs2Dj6f5yQzXuH/dZLsuM7XaqIc4KuIFc
29 | 9EjkdL4udt6NPJCuoASb5dHDUgX4J8rPij7MG9G6DErQvNUs9qd1rqhW/yoKbtBC
30 | DfeR08MLeT0pAQiO9syFNNq9UYOL9A1v9zY6WVpBqczGm3gumJmBCoBjCn5E7p5i
31 | H4GOH2r0cvIe6dwmyTb3kLaCJt13ZPIqcMknezf3cMjZHXR+TZXxOztm5VY8jtY5
32 | d3s0JWY5321C5JkhiGhvOqbAiaYDkTie+Ww5zL8D4R8YNYFaNMVFHsR5/QIDAQAB
33 | o1MwUTAdBgNVHQ4EFgQUUw15Nn7A8sG7pHEIrjDnWTrWKuQwHwYDVR0jBBgwFoAU
34 | Uw15Nn7A8sG7pHEIrjDnWTrWKuQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B
35 | AQsFAAOCAQEAeyuJd5H+E2TEXl+o6u/0dSx/UO1OXjKNAznF6GZ6QVP2r3WxObUe
36 | fdZnCpL9a4XkMFEG3WEEqgQZEvgLIgsGgCgkThrZHQcMoUQZnmLpiCYrA2ADQPv1
37 | gTzDO5UBp6itJ0l6RopYn67zH5aWDkZkCmRDmdj7mb22E7fsaugMh9YeJlXed+FN
38 | oEjOxy1+y5DDjF1eZU8V765fc5dYDQg73bW2con6vPTmD5MUStNhX533GOWzGyii
39 | z2ZHKM/JVZ9lRDFYp0Rs5L6sa1DMDhmr4Eb3oJvTXvGvTFKzFltOilVm9F22ZbYh
40 | mIb/1XIdA9EKiD26Vy+B5d7FQqBE8BlYPA==
41 | -----END CERTIFICATE-----
42 |
--------------------------------------------------------------------------------
/go/signedexchange/certurl/test-cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIF8jCCBNqgAwIBAgIQDmTF+8I2reFLFyrrQceMsDANBgkqhkiG9w0BAQsFADBw
3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
4 | d3cuZGlnaWNlcnQuY29tMS8wLQYDVQQDEyZEaWdpQ2VydCBTSEEyIEhpZ2ggQXNz
5 | dXJhbmNlIFNlcnZlciBDQTAeFw0xNTExMDMwMDAwMDBaFw0xODExMjgxMjAwMDBa
6 | MIGlMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEUMBIGA1UEBxML
7 | TG9zIEFuZ2VsZXMxPDA6BgNVBAoTM0ludGVybmV0IENvcnBvcmF0aW9uIGZvciBB
8 | c3NpZ25lZCBOYW1lcyBhbmQgTnVtYmVyczETMBEGA1UECxMKVGVjaG5vbG9neTEY
9 | MBYGA1UEAxMPd3d3LmV4YW1wbGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
10 | MIIBCgKCAQEAs0CWL2FjPiXBl61lRfvvE0KzLJmG9LWAC3bcBjgsH6NiVVo2dt6u
11 | Xfzi5bTm7F3K7srfUBYkLO78mraM9qizrHoIeyofrV/n+pZZJauQsPjCPxMEJnRo
12 | D8Z4KpWKX0LyDu1SputoI4nlQ/htEhtiQnuoBfNZxF7WxcxGwEsZuS1KcXIkHl5V
13 | RJOreKFHTaXcB1qcZ/QRaBIv0yhxvK1yBTwWddT4cli6GfHcCe3xGMaSL328Fgs3
14 | jYrvG29PueB6VJi/tbbPu6qTfwp/H1brqdjh29U52Bhb0fJkM9DWxCP/Cattcc7a
15 | z8EXnCO+LK8vkhw/kAiJWPKx4RBvgy73nwIDAQABo4ICUDCCAkwwHwYDVR0jBBgw
16 | FoAUUWj/kK8CB3U8zNllZGKiErhZcjswHQYDVR0OBBYEFKZPYB4fLdHn8SOgKpUW
17 | 5Oia6m5IMIGBBgNVHREEejB4gg93d3cuZXhhbXBsZS5vcmeCC2V4YW1wbGUuY29t
18 | ggtleGFtcGxlLmVkdYILZXhhbXBsZS5uZXSCC2V4YW1wbGUub3Jngg93d3cuZXhh
19 | bXBsZS5jb22CD3d3dy5leGFtcGxlLmVkdYIPd3d3LmV4YW1wbGUubmV0MA4GA1Ud
20 | DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdQYDVR0f
21 | BG4wbDA0oDKgMIYuaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItaGEtc2Vy
22 | dmVyLWc0LmNybDA0oDKgMIYuaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTIt
23 | aGEtc2VydmVyLWc0LmNybDBMBgNVHSAERTBDMDcGCWCGSAGG/WwBATAqMCgGCCsG
24 | AQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAgGBmeBDAECAjCB
25 | gwYIKwYBBQUHAQEEdzB1MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy
26 | dC5jb20wTQYIKwYBBQUHMAKGQWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9E
27 | aWdpQ2VydFNIQTJIaWdoQXNzdXJhbmNlU2VydmVyQ0EuY3J0MAwGA1UdEwEB/wQC
28 | MAAwDQYJKoZIhvcNAQELBQADggEBAISomhGn2L0LJn5SJHuyVZ3qMIlRCIdvqe0Q
29 | 6ls+C8ctRwRO3UU3x8q8OH+2ahxlQmpzdC5al4XQzJLiLjiJ2Q1p+hub8MFiMmVP
30 | PZjb2tZm2ipWVuMRM+zgpRVM6nVJ9F3vFfUSHOb4/JsEIUvPY+d8/Krc+kPQwLvy
31 | ieqRbcuFjmqfyPmUv1U9QoI4TQikpw7TZU0zYZANP4C/gj4Ry48/znmUaRvy2kvI
32 | l7gRQ21qJTK5suoiYoYNo3J9T+pXPGU7Lydz/HwW+w0DpArtAaukI8aNX4ohFUKS
33 | wDSiIIWIWJiJGbEeIO0TIFwEVWTOnbNl/faPXpk5IRXicapqiII=
34 | -----END CERTIFICATE-----
35 | -----BEGIN CERTIFICATE-----
36 | MIIEsTCCA5mgAwIBAgIQBOHnpNxc8vNtwCtCuF0VnzANBgkqhkiG9w0BAQsFADBs
37 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
38 | d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
39 | ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowcDEL
40 | MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
41 | LmRpZ2ljZXJ0LmNvbTEvMC0GA1UEAxMmRGlnaUNlcnQgU0hBMiBIaWdoIEFzc3Vy
42 | YW5jZSBTZXJ2ZXIgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2
43 | 4C/CJAbIbQRf1+8KZAayfSImZRauQkCbztyfn3YHPsMwVYcZuU+UDlqUH1VWtMIC
44 | Kq/QmO4LQNfE0DtyyBSe75CxEamu0si4QzrZCwvV1ZX1QK/IHe1NnF9Xt4ZQaJn1
45 | itrSxwUfqJfJ3KSxgoQtxq2lnMcZgqaFD15EWCo3j/018QsIJzJa9buLnqS9UdAn
46 | 4t07QjOjBSjEuyjMmqwrIw14xnvmXnG3Sj4I+4G3FhahnSMSTeXXkgisdaScus0X
47 | sh5ENWV/UyU50RwKmmMbGZJ0aAo3wsJSSMs5WqK24V3B3aAguCGikyZvFEohQcft
48 | bZvySC/zA/WiaJJTL17jAgMBAAGjggFJMIIBRTASBgNVHRMBAf8ECDAGAQH/AgEA
49 | MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw
50 | NAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy
51 | dC5jb20wSwYDVR0fBEQwQjBAoD6gPIY6aHR0cDovL2NybDQuZGlnaWNlcnQuY29t
52 | L0RpZ2lDZXJ0SGlnaEFzc3VyYW5jZUVWUm9vdENBLmNybDA9BgNVHSAENjA0MDIG
53 | BFUdIAAwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQ
54 | UzAdBgNVHQ4EFgQUUWj/kK8CB3U8zNllZGKiErhZcjswHwYDVR0jBBgwFoAUsT7D
55 | aQP4v0cB1JgmGggC72NkK8MwDQYJKoZIhvcNAQELBQADggEBABiKlYkD5m3fXPwd
56 | aOpKj4PWUS+Na0QWnqxj9dJubISZi6qBcYRb7TROsLd5kinMLYBq8I4g4Xmk/gNH
57 | E+r1hspZcX30BJZr01lYPf7TMSVcGDiEo+afgv2MW5gxTs14nhr9hctJqvIni5ly
58 | /D6q1UEL2tU2ob8cbkdJf17ZSHwD2f2LSaCYJkJA69aSEaRkCldUxPUd1gJea6zu
59 | xICaEnL6VpPX/78whQYwvwt/Tv9XBZ0k7YXDK/umdaisLRbvfXknsuvCnQsH6qqF
60 | 0wGjIChBWUMo0oHjqvbsezt3tkBigAVBRQHvFwY+3sAzm2fTYS5yh+Rp/BIAV0Ae
61 | cPUeybQ=
62 | -----END CERTIFICATE-----
63 |
--------------------------------------------------------------------------------
/go/signedexchange/cmd/dump-certurl/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "log"
6 | "os"
7 |
8 | "github.com/WICG/webpackage/go/signedexchange/certurl"
9 | )
10 |
11 | var (
12 | flagInput = flag.String("i", "", "Cert-chain CBOR input file")
13 | )
14 |
15 | func run() error {
16 | in := os.Stdin
17 | if *flagInput != "" {
18 | var err error
19 | in, err = os.Open(*flagInput)
20 | if err != nil {
21 | return err
22 | }
23 | defer in.Close()
24 | }
25 |
26 | chain, err := certurl.ReadCertChain(in)
27 | if err != nil {
28 | return err
29 | }
30 | chain.PrettyPrint(os.Stdout)
31 |
32 | return nil
33 | }
34 |
35 | func main() {
36 | flag.Parse()
37 | if err := run(); err != nil {
38 | log.Fatal(err)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/go/signedexchange/cmd/gen-certurl/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "flag"
6 | "fmt"
7 | "io/ioutil"
8 | "log"
9 | "os"
10 | "path/filepath"
11 |
12 | "golang.org/x/crypto/ocsp"
13 |
14 | "github.com/WICG/webpackage/go/internal/signingalgorithm"
15 | "github.com/WICG/webpackage/go/signedexchange/certurl"
16 | )
17 |
18 | var (
19 | pemFilepath = flag.String("pem", "", "PEM filepath")
20 | ocspFilepath = flag.String("ocsp", "", "DER-encoded OCSP response file. If omitted, fetched from network")
21 | preferGET = flag.Bool("preferGET", false, "Use GET if possible when fetching OCSP response from network")
22 | sctDirpath = flag.String("sctDir", "", "Directory containing .sct files")
23 | )
24 |
25 | func run(pemFilePath, ocspFilePath, sctDirPath string) error {
26 | pem, err := ioutil.ReadFile(pemFilePath)
27 | if err != nil {
28 | return err
29 | }
30 | certs, err := signingalgorithm.ParseCertificates(pem)
31 | if err != nil {
32 | return err
33 | }
34 | if len(certs) == 0 {
35 | return fmt.Errorf("input file %q has no certificates.", pemFilePath)
36 | }
37 |
38 | var ocspDer []byte
39 | if *ocspFilepath == "" {
40 | ocspDer, err = certurl.FetchOCSPResponse(certs, *preferGET)
41 | if err != nil {
42 | return err
43 | }
44 | } else {
45 | ocspDer, err = ioutil.ReadFile(ocspFilePath)
46 | if err != nil {
47 | return err
48 | }
49 | }
50 | parsedOcsp, err := ocsp.ParseResponse(ocspDer, nil)
51 | if err != nil {
52 | fmt.Fprintln(os.Stderr, "Warning: ocsp is not a correct DER-encoded OCSP response.")
53 | }
54 |
55 | var sctList []byte
56 | if sctDirPath != "" {
57 | files, err := filepath.Glob(filepath.Join(sctDirPath, "*.sct"))
58 | if err != nil {
59 | return err
60 | }
61 | scts := [][]byte{}
62 | for _, file := range files {
63 | sct, err := ioutil.ReadFile(file)
64 | if err != nil {
65 | return err
66 | }
67 | scts = append(scts, sct)
68 | }
69 | sctList, err = certurl.SerializeSCTList(scts)
70 | if err != nil {
71 | return err
72 | }
73 | } else {
74 | if !certurl.HasEmbeddedSCT(certs[0], parsedOcsp) {
75 | fmt.Fprintln(os.Stderr, "Warning: Neither cert nor OCSP have embedded SCT list. Use -sctDir flag to add SCT from files.")
76 | }
77 | }
78 | certChain, err := certurl.NewCertChain(certs, ocspDer, sctList)
79 | if err != nil {
80 | return err
81 | }
82 |
83 | buf := &bytes.Buffer{}
84 | if err := certChain.Write(buf); err != nil {
85 | return err
86 | }
87 |
88 | if _, err := buf.WriteTo(os.Stdout); err != nil {
89 | return err
90 | }
91 | return nil
92 | }
93 |
94 | func main() {
95 | flag.Parse()
96 | if *pemFilepath == "" {
97 | flag.Usage()
98 | return
99 | }
100 |
101 | if err := run(*pemFilepath, *ocspFilepath, *sctDirpath); err != nil {
102 | log.Fatal(err)
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/go/signedexchange/internal/bigendian/bigendianint.go:
--------------------------------------------------------------------------------
1 | package bigendian
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | var ErrOutOfRange = errors.New("bigendian: Given integer is out of encodable range.")
8 |
9 | func EncodeBytesUint(n int64, size int) ([]byte, error) {
10 | if n < 0 {
11 | return nil, ErrOutOfRange
12 | }
13 | if size < 7 && n > int64(1)<> uint((size-i-1)*8) & 0xff)
20 | }
21 | return bs, nil
22 | }
23 |
24 | func Decode3BytesUint(b [3]byte) int {
25 | return int(b[0])<<16 | int(b[1])<<8 | int(b[2])
26 | }
27 |
--------------------------------------------------------------------------------
/go/signedexchange/internal/bigendian/bigendianint_test.go:
--------------------------------------------------------------------------------
1 | package bigendian_test
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 |
7 | "github.com/WICG/webpackage/go/signedexchange/internal/bigendian"
8 | )
9 |
10 | func TestEncodeBytesUint(t *testing.T) {
11 | b, err := bigendian.EncodeBytesUint(0x123456, 3)
12 | if !bytes.Equal(b[:], []byte{0x12, 0x34, 0x56}) {
13 | t.Errorf("unexpected bytes: got %v", b)
14 | return
15 | }
16 | if err != nil {
17 | t.Errorf("unexpected err: %v", err)
18 | return
19 | }
20 |
21 | if _, err = bigendian.EncodeBytesUint(0x12345678, 3); err != bigendian.ErrOutOfRange {
22 | t.Errorf("unexpected err: %v", err)
23 | }
24 |
25 | b, err = bigendian.EncodeBytesUint(0x123456, 8)
26 | if !bytes.Equal(b[:], []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56}) {
27 | t.Errorf("unexpected bytes: got %v", b)
28 | return
29 | }
30 |
31 | b, err = bigendian.EncodeBytesUint(0x7ffffffffffffffe, 8)
32 | if !bytes.Equal(b[:], []byte{0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe}) {
33 | t.Errorf("unexpected bytes: got %v", b)
34 | return
35 | }
36 | }
37 |
38 | func TestDecode3BytesUint(t *testing.T) {
39 | expected := 0xabcdef
40 | actual := bigendian.Decode3BytesUint([...]byte{0xab, 0xcd, 0xef})
41 | if expected != actual {
42 | t.Errorf("expected decoded value %v but got %v", expected, actual)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/go/signedexchange/stateful_headers.go:
--------------------------------------------------------------------------------
1 | package signedexchange
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strings"
7 | )
8 |
9 | // https://jyasskin.github.io/webpackage/implementation-draft/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#stateful-headers.
10 | var statefulRequestHeadersSet map[string]struct{}
11 | var uncachedHeadersSet map[string]struct{}
12 |
13 | func init() {
14 | statefulRequestHeaders := []string{
15 | "authorization",
16 | "cookie",
17 | "cookie2",
18 | "proxy-authorization",
19 | "sec-websocket-key",
20 | }
21 | statefulRequestHeadersSet = make(map[string]struct{})
22 | for _, e := range statefulRequestHeaders {
23 | statefulRequestHeadersSet[e] = struct{}{}
24 | }
25 |
26 | uncachedHeaders := []string{
27 | // "Hop-by-hop header fields listed in the Connection header field
28 | // (Section 6.1 of {{!RFC7230}})." [spec text]
29 | // Note: The Connection header field itself is banned as uncached headers, so no-op.
30 |
31 | // "Header fields listed in the no-cache response directive in the
32 | // "Cache-Control header field (Section 5.2.2.2 of {{!RFC7234}})."
33 | // [spec text]
34 | // Note: This is to be handled specifically in VerifyUncachedHeader, but
35 | // is not currently implemented.
36 |
37 | // "Header fields defined as hop-by-hop" [spec text] and the entries from
38 | // the spec.
39 | "connection",
40 | "keep-alive",
41 | "proxy-connection",
42 | "trailer",
43 | "transfer-encoding",
44 | "upgrade",
45 |
46 | // "Stateful headers" [spec text]
47 | // draft-yasskin-http-origin-signed-responses.html#stateful-headers
48 | "authentication-control",
49 | "authentication-info",
50 | "clear-site-data",
51 | "optional-www-authenticate",
52 | "proxy-authenticate",
53 | "proxy-authentication-info",
54 | "public-key-pins",
55 | "sec-websocket-accept",
56 | "set-cookie",
57 | "set-cookie2",
58 | "setprofile",
59 | "strict-transport-security",
60 | "www-authenticate",
61 | }
62 | uncachedHeadersSet = make(map[string]struct{})
63 | for _, e := range uncachedHeaders {
64 | uncachedHeadersSet[e] = struct{}{}
65 | }
66 | }
67 |
68 | // IsStatefulRequestHeader returns true if the HTTP header n is considered stateful and is not allowed to be included in a signed exchange
69 | // Note that this only applies to signed exchanges of versions 1b1 and 1b2.
70 | func IsStatefulRequestHeader(n string) bool {
71 | cname := strings.ToLower(n)
72 | _, exists := statefulRequestHeadersSet[cname]
73 | return exists
74 | }
75 |
76 | func IsUncachedHeader(n string) bool {
77 | cname := strings.ToLower(n)
78 | _, exists := uncachedHeadersSet[cname]
79 | return exists
80 | }
81 |
82 | // VerifyUncachedHeader returns non-nil error if h has any uncached header fields as specified in
83 | // draft-yasskin-http-origin-signed-responses.html#uncached-headers
84 | func VerifyUncachedHeader(h http.Header) error {
85 | // TODO: Implement https://tools.ietf.org/html/rfc7234#section-5.2.2.2
86 |
87 | for n := range h {
88 | if IsUncachedHeader(n) {
89 | return fmt.Errorf("signedexchange: uncached header %q can't be captured inside a signed exchange.", n)
90 | }
91 | }
92 | return nil
93 | }
94 |
--------------------------------------------------------------------------------
/go/signedexchange/structuredheader/writer.go:
--------------------------------------------------------------------------------
1 | package structuredheader
2 |
3 | import (
4 | "encoding/base64"
5 | "errors"
6 | "fmt"
7 | "sort"
8 | "strconv"
9 | "strings"
10 | "unicode"
11 | )
12 |
13 | func (ll ListOfLists) String() (string, error) {
14 | var b strings.Builder
15 | if err := ll.serialize(&b); err != nil {
16 | return "", err
17 | }
18 | return b.String(), nil
19 | }
20 |
21 | func (pl ParameterisedList) String() (string, error) {
22 | var b strings.Builder
23 | if err := pl.serialize(&b); err != nil {
24 | return "", err
25 | }
26 | return b.String(), nil
27 | }
28 |
29 | func (pi *ParameterisedIdentifier) String() (string, error) {
30 | var b strings.Builder
31 | if err := pi.serialize(&b); err != nil {
32 | return "", err
33 | }
34 | return b.String(), nil
35 | }
36 |
37 | // https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-09#section-4.1.3
38 | func (ll ListOfLists) serialize(out *strings.Builder) error {
39 | if len(ll) == 0 {
40 | return errors.New("structuredheader: empty List of Lists")
41 | }
42 |
43 | outerSep := ""
44 | for _, list := range ll {
45 | if len(list) == 0 {
46 | return errors.New("structuredheader: empty inner list in List of Lists")
47 | }
48 |
49 | out.WriteString(outerSep)
50 | outerSep = ", "
51 |
52 | innerSep := ""
53 | for _, item := range list {
54 | out.WriteString(innerSep)
55 | innerSep = "; "
56 |
57 | if err := serializeItem(item, out); err != nil {
58 | return err
59 | }
60 | }
61 | }
62 | return nil
63 | }
64 |
65 | // https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-09#section-4.1.4
66 | func (pl ParameterisedList) serialize(out *strings.Builder) error {
67 | if len(pl) == 0 {
68 | return errors.New("structuredheader: empty Parameterised List")
69 | }
70 |
71 | sep := ""
72 | for _, pi := range pl {
73 | out.WriteString(sep)
74 | sep = ", "
75 | if err := pi.serialize(out); err != nil {
76 | return err
77 | }
78 | }
79 | return nil
80 | }
81 |
82 | // Step 2.1-2.3 of
83 | // https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-09#section-4.1.4
84 | func (pi *ParameterisedIdentifier) serialize(out *strings.Builder) error {
85 | if !isValidToken(string(pi.Label)) {
86 | return fmt.Errorf("structuredheader: label %q is not a valid token", pi.Label)
87 | }
88 | out.WriteString(string(pi.Label))
89 |
90 | // Format in sorted order for reproducibility.
91 | var keys []string
92 | for k := range pi.Params {
93 | keys = append(keys, string(k))
94 | }
95 | sort.Strings(keys)
96 |
97 | for _, k := range keys {
98 | out.WriteByte(';')
99 | if !isValidKey(k) {
100 | return fmt.Errorf("structuredheader: invalid key %q", k)
101 | }
102 | out.WriteString(k)
103 | val := pi.Params[Key(k)]
104 | if val != nil {
105 | out.WriteByte('=')
106 | if err := serializeItem(val, out); err != nil {
107 | return err
108 | }
109 | }
110 | }
111 | return nil
112 | }
113 |
114 | // https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-09#section-4.1.5
115 | func serializeItem(i Item, out *strings.Builder) error {
116 | switch v := i.(type) {
117 | case int64:
118 | // https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-09#section-4.1.6
119 | out.WriteString(strconv.FormatInt(v, 10))
120 | return nil
121 |
122 | case string:
123 | // https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-09#section-4.1.8
124 | for _, c := range v {
125 | if c < ' ' || c > '~' {
126 | return fmt.Errorf("structuredheader: couldn't serialize %q as string", v)
127 | }
128 | }
129 | out.WriteString(strconv.Quote(v))
130 | return nil
131 |
132 | case Token:
133 | // https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-09#section-4.1.9
134 | if !isValidToken(string(v)) {
135 | return fmt.Errorf("structuredheader: couldn't serialize %q as token", v)
136 | }
137 | out.WriteString(string(v))
138 | return nil
139 |
140 | case []byte:
141 | // https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-09#section-4.1.10
142 | out.WriteByte('*')
143 | out.WriteString(base64.StdEncoding.EncodeToString(v))
144 | out.WriteByte('*')
145 | return nil
146 |
147 | default:
148 | return fmt.Errorf("structuredheader: couldn't serialize %v as item", i)
149 | }
150 | }
151 |
152 | func isValidKey(s string) bool {
153 | if len(s) == 0 || !isLCAlpha(s[0]) {
154 | return false
155 | }
156 | for _, c := range s {
157 | if c > unicode.MaxASCII || !isKeyChar(byte(c)) {
158 | return false
159 | }
160 | }
161 | return true
162 | }
163 |
164 | func isValidToken(s string) bool {
165 | if len(s) == 0 || !isAlpha(s[0]) {
166 | return false
167 | }
168 | for _, c := range s {
169 | if c > unicode.MaxASCII || !isTokenChar(byte(c)) {
170 | return false
171 | }
172 | }
173 | return true
174 | }
175 |
--------------------------------------------------------------------------------
/go/signedexchange/structuredheader/writer_test.go:
--------------------------------------------------------------------------------
1 | package structuredheader_test
2 |
3 | import (
4 | . "github.com/WICG/webpackage/go/signedexchange/structuredheader"
5 | "testing"
6 | )
7 |
8 | func TestSerializeItem(t *testing.T) {
9 | cases := []struct {
10 | input Item
11 | expected string // empty if serialization should fail
12 | }{
13 | {int64(42), "42"},
14 | {int64(9223372036854775807), "9223372036854775807"}, // int64 max
15 | {int64(-9223372036854775808), "-9223372036854775808"}, // int64 min
16 | {"string", `"string"`},
17 | {"", `""`},
18 | {"\x7f", ""},
19 | {"\x1f", ""},
20 | {`foo"bar`, `"foo\"bar"`},
21 | {`foo\bar`, `"foo\\bar"`},
22 | {`foo\"bar`, `"foo\\\"bar"`},
23 | {" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~", `" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~"`},
24 | {"\u65e5\u672c\u8a9e", ""},
25 | {Token("foo"), "foo"},
26 | {Token("BAR"), "BAR"},
27 | {Token("A123_-.:%*/"), "A123_-.:%*/"},
28 | {Token("123"), ""},
29 | {Token("_foo"), ""},
30 | {Token(""), ""},
31 | {[]byte{}, "**"},
32 | {[]byte("foo"), "*Zm9v*"},
33 | {[]byte("hoge"), "*aG9nZQ==*"},
34 | }
35 | for _, c := range cases {
36 | s, err := ListOfLists{{c.input}}.String()
37 | if c.expected != "" {
38 | if err != nil {
39 | t.Errorf("Unexpetedly failed to serialize %v: %v", c.input, err)
40 | }
41 | if s != c.expected {
42 | t.Errorf("%v should serialize to %q, but got %q", c.input, c.expected, s)
43 | }
44 | } else {
45 | if err == nil {
46 | t.Errorf("serialization of %v did not fail", c.input)
47 | }
48 | }
49 | }
50 | }
51 |
52 | func TestSerializeListOfLists(t *testing.T) {
53 | cases := []struct {
54 | input ListOfLists
55 | expected string // empty if serialization should fail
56 | }{
57 | {ListOfLists{{}}, ""},
58 | {ListOfLists{{int64(42)}}, "42"},
59 | {ListOfLists{{int64(42)}, {}}, ""},
60 | {ListOfLists{{int64(42), Token("foo")}}, "42; foo"},
61 | {ListOfLists{{int64(42)}, {Token("foo")}}, "42, foo"},
62 | {ListOfLists{{int64(42), int64(-42)}, {Token("foo"), Token("bar")}}, "42; -42, foo; bar"},
63 | }
64 | for _, c := range cases {
65 | s, err := c.input.String()
66 | if c.expected != "" {
67 | if err != nil {
68 | t.Errorf("Unexpetedly failed to serialize %v: %v", c.input, err)
69 | }
70 | if s != c.expected {
71 | t.Errorf("%v should serialize to %q, but got %q", c.input, c.expected, s)
72 | }
73 | } else {
74 | if err == nil {
75 | t.Errorf("serialization of %v did not fail", c.input)
76 | }
77 | }
78 | }
79 | }
80 |
81 | func TestSerializeParameterisedList(t *testing.T) {
82 | cases := []struct {
83 | input ParameterisedList
84 | expected string // empty if serialization should fail
85 | }{
86 | {ParameterisedList{}, ""},
87 | {ParameterisedList{{"label", Parameters{}}}, "label"},
88 | {ParameterisedList{{"_label", Parameters{}}}, ""},
89 | {ParameterisedList{{"item1", Parameters{"n": int64(123)}}}, "item1;n=123"},
90 | {ParameterisedList{
91 | {"item1", Parameters{"n": int64(123)}},
92 | {"item2", Parameters{}},
93 | {"item3", Parameters{"n": int64(456)}},
94 | }, "item1;n=123, item2, item3;n=456"},
95 | {ParameterisedList{{"item1", Parameters{"InvalidKey": int64(123)}}}, ""},
96 | }
97 | for _, c := range cases {
98 | s, err := c.input.String()
99 | if c.expected != "" {
100 | if err != nil {
101 | t.Errorf("Unexpetedly failed to serialize %v: %v", c.input, err)
102 | }
103 | if s != c.expected {
104 | t.Errorf("%v should serialize to %q, but got %q", c.input, c.expected, s)
105 | }
106 | } else {
107 | if err == nil {
108 | t.Errorf("serialization of %v did not fail", c.input)
109 | }
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/go/signedexchange/test-signedexchange-expected-payload-mi.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WICG/webpackage/5262e60be64301517a51577c81421d1422fb76fd/go/signedexchange/test-signedexchange-expected-payload-mi.bin
--------------------------------------------------------------------------------
/go/signedexchange/version/version.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 |
7 | "github.com/WICG/webpackage/go/signedexchange/mice"
8 | )
9 |
10 | type Version string
11 |
12 | const (
13 | Version1b1 Version = "1b1"
14 | Version1b2 Version = "1b2"
15 | Version1b3 Version = "1b3"
16 | )
17 |
18 | const HeaderMagicBytesLen = 8
19 |
20 | var AllVersions = []Version{
21 | Version1b1,
22 | Version1b2,
23 | Version1b3,
24 | }
25 |
26 | func Parse(str string) (Version, bool) {
27 | switch Version(str) {
28 | case Version1b1:
29 | return Version1b1, true
30 | case Version1b2:
31 | return Version1b2, true
32 | case Version1b3:
33 | return Version1b3, true
34 | }
35 | return "", false
36 | }
37 |
38 | func (v Version) HeaderMagicBytes() []byte {
39 | switch v {
40 | case Version1b1:
41 | return []byte("sxg1-b1\x00")
42 | case Version1b2:
43 | return []byte("sxg1-b2\x00")
44 | case Version1b3:
45 | return []byte("sxg1-b3\x00")
46 | default:
47 | panic("not reached")
48 | }
49 | }
50 |
51 | func (v Version) MimeType() string {
52 | return fmt.Sprintf("application/signed-exchange;v=%s", v[1:])
53 | }
54 |
55 | func FromMagicBytes(bs []byte) (Version, error) {
56 | if bytes.Equal(bs, Version1b1.HeaderMagicBytes()) {
57 | return Version1b1, nil
58 | } else if bytes.Equal(bs, Version1b2.HeaderMagicBytes()) {
59 | return Version1b2, nil
60 | } else if bytes.Equal(bs, Version1b3.HeaderMagicBytes()) {
61 | return Version1b3, nil
62 | } else {
63 | return Version(""), fmt.Errorf("signedexchange: unknown magic bytes: %v", bs)
64 | }
65 | }
66 |
67 | func (v Version) MiceEncoding() mice.Encoding {
68 | switch v {
69 | case Version1b1:
70 | return mice.Draft02Encoding
71 | case Version1b2, Version1b3:
72 | return mice.Draft03Encoding
73 | default:
74 | panic("not reached")
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/js/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to wbn and wbn-sign packages
2 |
3 | Follow the [main contribution guidelines](../CONTRIBUTING.md). This file
4 | focuses on contribution instructions which are specific to the `js/` part of the
5 | repository containing the code for `wbn` and `wbn-sign` packages.
6 |
7 | ## Auto-formatting code
8 |
9 | The Github Actions workflow enforces linting code with Prettier according to the
10 | Prettier configs specified in the `package.json`.
11 |
12 | To lint your code locally before committing, one can run `npm run lint`.
13 |
14 | To enable running Prettier on save with VSCode, one can install the Prettier
15 | extension and then in VScode's settings have the following entries:
16 |
17 | ```json
18 | "editor.formatOnSave": true,
19 | "[javascript]": {
20 | "editor.defaultFormatter": "esbenp.prettier-vscode"
21 | }
22 | ```
23 |
--------------------------------------------------------------------------------
/js/bundle/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib
3 | wbn-*.tgz
--------------------------------------------------------------------------------
/js/bundle/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | tests
3 | tsconfig.json
4 | wbn*.tgz
5 | *.wbn
6 |
--------------------------------------------------------------------------------
/js/bundle/.prettierignore:
--------------------------------------------------------------------------------
1 | lib
2 | testdata
3 | *.wbn
4 | *.swbn
5 |
--------------------------------------------------------------------------------
/js/bundle/README.md:
--------------------------------------------------------------------------------
1 | # Web Bundles
2 |
3 | This is a Node.js module for serializing and parsing the `application/webbundle`
4 | format defined in the
5 | [Web Bundles](https://wpack-wg.github.io/bundled-responses/draft-ietf-wpack-bundled-responses.html)
6 | draft spec.
7 |
8 | ## Installation
9 |
10 | Using npm:
11 |
12 | ```bash
13 | npm install wbn
14 | ```
15 |
16 | ## Usage
17 |
18 | Please be aware that the API is not yet stable and is subject to change any
19 | time.
20 |
21 | Creating a Bundle:
22 |
23 | ```javascript
24 | import * as fs from 'fs';
25 | import * as wbn from 'wbn';
26 |
27 | const builder = new wbn.BundleBuilder();
28 | builder.addExchange(
29 | 'https://example.com/', // URL
30 | 200, // response code
31 | { 'Content-Type': 'text/html' }, // response headers
32 | 'Hello, Web Bundle!' // response body (string or Uint8Array)
33 | );
34 | builder.setPrimaryURL('https://example.com/'); // entry point URL
35 |
36 | fs.writeFileSync('out.wbn', builder.createBundle());
37 | ```
38 |
39 | Reading a Bundle:
40 |
41 | ```javascript
42 | import * as fs from 'fs';
43 | import * as wbn from 'wbn';
44 |
45 | const buf = fs.readFileSync('out.wbn');
46 | const bundle = new wbn.Bundle(buf);
47 | const exchanges = [];
48 | for (const url of bundle.urls) {
49 | const resp = bundle.getResponse(url);
50 | exchanges.push({
51 | url,
52 | status: resp.status,
53 | headers: resp.headers,
54 | body: new TextDecoder('utf-8').decode(resp.body),
55 | });
56 | }
57 | console.log(
58 | JSON.stringify(
59 | {
60 | version: bundle.version, // format version
61 | exchanges,
62 | },
63 | null,
64 | 2
65 | )
66 | );
67 | ```
68 |
69 | ## CLI
70 |
71 | This package also includes `wbn` command which lets you build a web bundle from
72 | a local directory. For example, if you have all the necessary files for
73 | `https://example.com/` in `static/` directory, run the following command:
74 |
75 | ```sh
76 | $ wbn --dir static \
77 | --baseURL https://example.com/ \
78 | --output out.wbn
79 | ```
80 |
81 | Run `wbn --help` for full options.
82 |
83 | Note: currently this CLI only covers a subset of the functionality offered by
84 | [`gen-bundle`](https://github.com/WICG/webpackage/tree/master/go/bundle#gen-bundle)
85 | Go tool.
86 |
87 | ### Backwards compatibility
88 |
89 | This module supports creating and parsing Web Bundles that follow different
90 | draft versions of the format specification. In particular:
91 |
92 | - version `b2` follows
93 | [the latest version of the Web Bundles spec](https://datatracker.ietf.org/doc/html/draft-ietf-wpack-bundled-responses)
94 | (default)
95 | - version `b1` follows
96 | [the previous version of the Web Bundles spec](https://datatracker.ietf.org/doc/html/draft-yasskin-wpack-bundled-exchanges-03)
97 |
98 | To create a new bundle with the `b1` format, pass the version value to the
99 | constructor:
100 |
101 | ```javascript
102 | const builder = (new wbn.BundleBuilder('b1'))
103 | .setPrimaryURL('https://example.com/')
104 | .setManifestURL('https://example.com/manifest.json')
105 | .addExchange(...);
106 |
107 | fs.writeFileSync('out_b1.wbn', builder.createBundle());
108 | ```
109 |
110 | Likewise, the `wbn` command can optionally take a `--formatVersion b1` parameter
111 | when creating a new Web Bundle.
112 |
113 | This module also takes care of selecting the right format version automatically
114 | when reading a bundle. Check the property `bundle.version` to know the decoded
115 | bundle's format version.
116 |
117 | ## Using Bundles
118 |
119 | Generated bundles can be opened with web browsers supporting web bundles.
120 |
121 | Chrome (79+) experimentally supports navigation to Web Bundles with some
122 | limitations. See
123 | [this document](https://chromium.googlesource.com/chromium/src/+/refs/heads/master/content/browser/web_package/using_web_bundles.md)
124 | for more details.
125 |
126 | Chrome (104+) supports
127 | [`