diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f1a475..6b65125 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,31 +6,47 @@ on: pull_request: branches: [main] +# Least privilege: this workflow only reads the repo and uploads artifacts. +permissions: + contents: read + jobs: wordpress: name: WordPress Plugin runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: persist-credentials: false - - uses: shivammathur/setup-php@v2 + - uses: shivammathur/setup-php@b604ade2a87db23f8871b7182e69ec5e75effb45 # v2 with: php-version: "8.3" extensions: intl, mbstring, mysqli tools: composer - - name: Configure private dep access - env: - TOKEN: ${{ secrets.HTMLTRUST_PKG_TOKEN }} - run: | - git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" - composer config --global github-oauth.github.com "${TOKEN}" - + # The package token used to be written to ~/.gitconfig and to composer's + # global auth.json, where every later step -- including any composer + # plugin or lifecycle script -- could read it back. It is now passed + # through COMPOSER_AUTH and GIT_CONFIG_*, which are scoped to this + # process tree and never persisted to disk. --no-scripts and --no-plugins + # stop third-party install hooks (dealerdirect/phpcodesniffer-composer- + # installer among them) from running while the token is in the + # environment; vendor/ is excluded from the packaged zip below, so + # nothing downstream depends on those hooks having run. + # + # HTMLTRUST_PKG_TOKEN must be a fine-grained PAT scoped to the + # HTMLTrust/htmltrust-canonicalization repository with Contents: Read and + # nothing else. A classic `repo`-scoped token grants write access to every + # repo the owner can reach and must not be used here. - name: Install dependencies working-directory: wordpress - run: composer install --no-interaction --prefer-dist + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.HTMLTRUST_PKG_TOKEN }}"}}' + GIT_CONFIG_COUNT: "1" + GIT_CONFIG_KEY_0: url.https://x-access-token:${{ secrets.HTMLTRUST_PKG_TOKEN }}@github.com/.insteadOf + GIT_CONFIG_VALUE_0: https://github.com/ + run: composer install --no-interaction --prefer-dist --no-scripts --no-plugins - name: Package plugin run: | @@ -39,7 +55,7 @@ jobs: zip -r ../dist/htmltrust-wordpress-plugin.zip . \ -x "vendor/*" "tests/*" "bin/*" "composer.lock" "phpunit.xml" - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wordpress-plugin path: dist/htmltrust-wordpress-plugin.zip @@ -48,7 +64,9 @@ jobs: name: Hugo Integration runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + persist-credentials: false - name: Verify Hugo partials syntax run: | @@ -57,7 +75,7 @@ jobs: test -s hugo/layouts/partials/htmltrust-meta.html && echo " ✓ htmltrust-meta.html" test -s hugo/scripts/sign-site.mjs && echo " ✓ sign-site.mjs" - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: hugo-integration path: | diff --git a/README.md b/README.md index d470ef3..1814003 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ htmltrust-cms-reference/ ### Prerequisites - WordPress 5.0+ -- PHP 7.0+ +- PHP 7.2+ - A running [HTMLTrust trust directory server](https://github.com/HTMLTrust/htmltrust-server-reference) ### Installation @@ -82,12 +82,15 @@ composer test ## The HTML Protocol -Signed content is embedded using data attributes that the browser extension recognizes: +Signed content is embedded with a `` wrapper around the actual signed content: ```html + content-hash="sha256:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU"> + + +

Verifiable Web Content

Content should be provable...

diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 1c89a4f..1210a43 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -139,7 +139,7 @@ Stores details about each signature generated. | signing_author_id | VARCHAR | External author ID | | wp_user_id | BIGINT | WordPress user ID | | content_hash | VARCHAR | Content hash | -| domain | VARCHAR | Domain | +| domain | VARCHAR | Serialized Web origin stored in the legacy-named API field | | signature | TEXT | Signature data | | claims_json | TEXT | Claims as JSON | | status | VARCHAR | Signature status | @@ -364,4 +364,4 @@ wp i18n make-json languages/content-signing-LOCALE.po - Keep API keys secure - Validate and sanitize all user input -- Follow WordPress security best practices \ No newline at end of file +- Follow WordPress security best practices diff --git a/docs/html-protocol.md b/docs/html-protocol.md index 3cc4f27..fb25ee9 100644 --- a/docs/html-protocol.md +++ b/docs/html-protocol.md @@ -4,7 +4,7 @@ This document specifies how content publishers embed cryptographic signatures in ## Overview -Signed content uses the `` custom HTML element, as defined in the [HTMLTrust specification](https://github.com/HTMLTrust/htmltrust-spec). This element wraps or accompanies signed content and carries the cryptographic signature as attributes. +Signed content uses the `` custom HTML element, as defined in the [HTMLTrust specification](https://github.com/HTMLTrust/htmltrust-spec). This element wraps the signed content and carries the cryptographic signature as attributes. ## The `` Element @@ -16,7 +16,7 @@ Per spec §2.1, the wrapper element carries exactly four required attributes: |---|---|---| | `keyid` | Identifies the signer; resolved per the rules in **Identity and Key Resolution** below. May be a DID, a direct URL to a public key document, or a trust-directory reference. | `keyid="did:web:author.example"` | | `signature` | Base64-encoded (unpadded) cryptographic signature over the canonical binding string defined in **Signature Data Format** | `signature="aBcDeF123..."` | -| `content-hash` | Hash of the canonicalized text content, prefixed with the hash algorithm | `content-hash="sha256:abc123def456..."` | +| `content-hash` | Hash of the canonicalized content, prefixed with the hash algorithm and encoded as unpadded standard Base64 | `content-hash="sha256:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU"` | | `algorithm` | Signature algorithm. Required by the spec; implementations MAY default to `ed25519` when the attribute is omitted, but producers SHOULD always emit it explicitly. | `algorithm="ed25519"` | ### Optional Attributes @@ -59,14 +59,14 @@ Custom claim types are permitted. The claim vocabulary is extensible. ## HTML Structure -The `` element can either **wrap** the signed content: +The `` element wraps the signed content: ```html + content-hash="sha256:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU"> @@ -79,26 +79,7 @@ The `` element can either **wrap** the signed content: ``` -Or appear as a **standalone marker** alongside content (e.g., when added by a CMS after the content): - -```html -
-

Verifiable Web Content

-

This content is signed and verifiable.

-
- - - - - - -``` - -Both forms are valid. Verifying clients should handle either case. +CMS integrations should not emit a detached `` containing only metadata. Compatibility UI such as badges and verification buttons should live outside the signed section. ## Identity and Key Resolution @@ -116,17 +97,18 @@ User agents MAY cache resolved keys (with appropriate freshness and revocation h ## Canonical Content Extraction -The hash that the signature covers is taken from the **text content** of the signed region, after the extraction and normalization process described below (spec §2.1). This is performed in two stages: HTML extraction, then text normalization. +The hash that the signature covers is taken from the canonicalized signed region after extraction and normalization. The canonical content includes normalized text plus the signed semantic attributes `href`, `src`, `alt`, and `aria-label` when present on included elements. ### Stage 1: HTML extraction Given the inner contents of a `` element: 1. **Strip excluded elements** entirely, including their text content: `