├── .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 | [`