Repository navigation
feat(signing): add a Signing chapter with a live SigV4/SigV4a signer - #16
Conversation
Bring the WebCrypto SigV4 and SigV4a signer from caller-identity, with its AWS test vectors, and build a Signing chapter on it: a signature lab for a long-term key, the AssumeRole call itself and temporary credentials, a presigned URL builder, SigV4a key derivation and a Bedrock API key bearer call. Fold the wire-level facts from caller-identity's overlapping paths into the EC2 and GitHub Actions paths and the trust chapter, move its research notes to docs/signing/, and stop deferring signing to the sibling project.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
|
|
||
| # Signing notes | ||
|
|
||
| These notes back the [Signing chapter](https://0-draft.github.io/aws-credentials/#signing). They were written for the former `caller-identity` project and moved here when it was merged into this one on 2026-11-08. |
There was a problem hiding this comment.
The link points to https://0-draft.github.io/aws-credentials/, but this site is served from https://0-draft.github.io/assume-role/ (README.md:8, vite.config.ts:7). The link is dead or points at the wrong project. README.ja.md:5 has the same problem.
There was a problem hiding this comment.
Intentional: this PR is the first step of renaming the repository to aws-credentials, and the Pages base path moves to /aws-credentials/ in the follow-up PR right after the rename, so the link is correct once that lands.
| offers itself as a test case. The canonical request, its hash, the signature and the | ||
| complete URL all match. | ||
|
|
||
| If either breaks, every signature the site renders is wrong, so both run as their own CI |
There was a problem hiding this comment.
This says the two AWS vectors "run as their own CI job", and line 114 says "Each check also runs as its own CI job". ci.yml has no such jobs. All vitest tests, including these vectors, run inside the single test job (npm run test:coverage). Only typecheck, lint, format, lint:md and build are matrix jobs. Either add the jobs or reword. 05-verification.ja.md:104 makes the same claim.
There was a problem hiding this comment.
Fixed in the latest commit: both files now say the vectors run in the single test job, with typecheck, lint, format, markdown lint and build as separate jobs.
| messages. A presigned URL pasted into a ticket is a credential in a ticket. | ||
|
|
||
| `X-Amz-Expires` is the only thing bounding the damage. S3 caps it at 604800 seconds, | ||
| seven days, because the signing key itself is valid only that long. |
There was a problem hiding this comment.
"because the signing key itself is valid only that long" reads as recalled, not sourced. AWS documents the 7-day maximum for SigV4 presigned URLs, but I don't know of a statement giving this reason. It also conflicts with the key chain in 02-sigv4.md, where the signing key is scoped to one date. Also, a URL presigned with STS temporary credentials stops working when those credentials expire, whatever X-Amz-Expires says. That is the practical bound for this repo's topic. Cite the source or drop the causal clause. The same sentence is in 04-variants.ja.md and in the PresignOptions doc comment in sigv4.ts.
There was a problem hiding this comment.
Fixed in the latest commit: dropped the unsourced reason. The notes, the lab copy and the sigv4.ts comment now say the CLI/SDKs allow up to 7 days (console 1 min to 12 h) and that a URL signed with temporary credentials dies with them, citing the S3 user guide.
|
Review of the Signing chapter. The SigV4 and SigV4a code and the EN/JA docs otherwise agree with each other. The derivation pseudocode and the key-chain description match AWS's published algorithm. I did not run the tests or check the AWS-published vectors against AWS's pages. Three findings, all inline:
|
| import { SigV4aLab } from "./SigV4aLab"; | ||
| import { Wire } from "./Wire"; | ||
|
|
||
| const REPO = "https://github.com/0-draft/aws-credentials/blob/main"; |
There was a problem hiding this comment.
REPO points at 0-draft/aws-credentials, but this repository is 0-draft/assume-role (index.html and vite.config.ts both serve it from /assume-role/). The "Verification" link at line 185 will go to a repo that isn't this one, and the file it names (docs/signing/05-verification.md) lives here. Use https://github.com/0-draft/assume-role/blob/main.
There was a problem hiding this comment.
Intentional, same as the docs link: the repository is renamed to aws-credentials right after this merges, and GitHub redirects the old blob URLs either way.
|
|
||
| # Signing notes | ||
|
|
||
| These notes back the [Signing chapter](https://0-draft.github.io/aws-credentials/#signing). They were written for the former `caller-identity` project and moved here when it was merged into this one on 2026-11-08. |
There was a problem hiding this comment.
The Signing chapter link goes to 0-draft.github.io/aws-credentials/#signing. The site is published at 0-draft.github.io/assume-role/ (index.html canonical URL), so this link is dead or points elsewhere. README.ja.md:5 has the same wrong host, …/aws-credentials/?lang=ja#signing.
There was a problem hiding this comment.
Intentional: the Pages base path moves to /aws-credentials/ in the follow-up PR right after the rename.
| already, so it is a step in a chain rather than an entry point. | ||
| - **OIDC federation.** Ends long-term keys in CI. The trust policy does the work, and | ||
| getting it wrong is the subject of [Federation](03-federation.md). | ||
| - **IMDS.** Why an EC2 instance or a Lambda needs nothing on disk. The `PUT` in IMDSv2 is |
There was a problem hiding this comment.
Lambda does not get credentials from IMDS. It injects AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN as environment variables. "An EC2 instance or a Lambda" under the IMDS bullet is therefore wrong. The JA twin (01-credential-map.ja.md:58) repeats it. Also, the same bullet's "PUT … not stylistic" reads fine, but IMDS is the only item listed with Lambda, which also contradicts the diagram at the top of the page showing IMDS / ECS endpoint only.
There was a problem hiding this comment.
Fixed in 26dc64d: the IMDS bullet is EC2-only now, with a note that Lambda gets the three values as environment variables and ECS / Pod Identity from a container endpoint (EN and JA).
| | --- | --- | --- | --- | | ||
| | Long-term key | `AKIA...` + secret | `iam:CreateAccessKey` | Never | | ||
| | Temporary credential | `ASIA...` + secret + session token | STS | Yes, always | | ||
| | Bearer token | One opaque string | Bedrock, CloudWatch | Depends | |
There was a problem hiding this comment.
"The long-term key is the only AWS API credential with no expiry" is stated flatly, but the table row above lists Bedrock bearer tokens as "Depends", and Bedrock's long-term API keys can be created without expiry. Either soften this or scope it to SigV4 key pairs. Also, "CloudWatch" as a bearer-token issuer is not sourced anywhere in the diff. Please cite or drop it.
There was a problem hiding this comment.
Fixed in 26dc64d: scoped to SigV4 key pairs, noted that a long-term Bedrock API key lasts until its configured expiry, and dropped CloudWatch.
|
|
||
| One consequence worth stating: `@noble/curves` defaults to the RFC 6979 deterministic | ||
| nonce, so signing the same request twice here gives identical bytes. AWS SDKs sign through | ||
| libcrypto and will not. Both verify; only one is reproducible, which matters if you ever |
There was a problem hiding this comment.
"AWS SDKs sign through libcrypto and will not [be deterministic]" is stated as fact for all SDKs and is not sourced. Only some SDKs (for example the C-based CRT) use libcrypto. Java, Go and JS use their own platform crypto. The same claim is in SigV4aLab.tsx:139-140 (including "produce different bytes … every time" in the JA string), and 05-verification.md rates it as a documented point. Per the verification ladder this is "weak", so phrase it as "typically randomised" or cite a source.
There was a problem hiding this comment.
Fixed in 26dc64d: reworded to 'SDK signers typically use a randomised nonce (unverified per SDK)' in the notes, the lab copy and the test comments.
| "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" | ||
| }, | ||
| "StringLike": { | ||
| "token.actions.githubusercontent.com:sub": "repo:0-draft/caller-identity:ref:refs/heads/main" |
There was a problem hiding this comment.
Item 3 claims repo:my-org/* "does not distinguish a pull request from a fork". Fork pull requests from outside the org do not get an id-token: write token by default, and their sub is built from the base repo, so the stated failure mode is unsourced and likely misleading. Cite GitHub's OIDC docs or reword to the distinction that does hold (pull_request vs ref:refs/heads/… subs).
There was a problem hiding this comment.
Fixed in 26dc64d: item 3 now says what does hold, that the wildcard matches every trigger, so a pull_request run gets the same role as a push to main.
|
Review of the Signing chapter merge. No CLAUDE.md, AGENTS.md or CONTRIBUTING.md exists, so I checked against the README and CI. EN/JA parity looked intact in the files I compared. Main findings (inline):
I did not verify the 2026 What's New claims (September 2026 token size limits, January 2026 claim validation) against the linked sources. |
… soften unsourced claims
|
Review of the Signing chapter. The SigV4 and SigV4a code reads correctly against the published algorithm, and the EN and JA README rows match. Findings, all inline:
I did not run the test suite. I read the SigV4 and SigV4a source and the docs, not the test vectors. |
| | `AssumeRoleWithSAML` | A SAML assertion, **no AWS credential** | Same | | ||
| | `AssumeRoleWithWebIdentity` | An OIDC ID token, **no AWS credential** | Same | | ||
| | `AssumeRoot` | Management account | Fixed 15 minutes | | ||
| | `GetSessionToken` | An IAM user key, optionally with MFA | 12 hours | |
There was a problem hiding this comment.
The 12 hour ceiling for GetSessionToken and GetFederationToken is wrong. 12 hours is the default. For an IAM user the maximum is 129600 s (36 h), and for the root user it is 3600 s. The repo's own notes say so in docs/03-api-operations.md:124-125 and docs/04-sessions-and-policies.md:430-431, and docs/01-overview.md:26 says "15 min to 36 h, default 12 h". Fix lines 17 and 18 and the JA table at 03-federation.ja.md:17-18. The AssumeRole paragraph just below makes the same kind of ceiling-versus-default distinction.
There was a problem hiding this comment.
Fixed: both rows now read 36 hours (default 12; root user 1 hour), EN and JA, matching docs/03-api-operations.md.
|
|
||
| # Signing notes | ||
|
|
||
| These notes back the [Signing chapter](https://0-draft.github.io/aws-credentials/#signing). They were written for the former `caller-identity` project and moved here when it was merged into this one on 2026-11-08. |
There was a problem hiding this comment.
The site URL is wrong. https://0-draft.github.io/aws-credentials/#signing is not where this site is served. Per vite.config.ts:7, index.html and README.md:8, it is https://0-draft.github.io/assume-role/. The same bad link is in docs/signing/README.ja.md:5, with ?lang=ja.
There was a problem hiding this comment.
Same as above: intentional, the rename to aws-credentials and the base-path PR follow immediately after this merge.
|
|
||
| ## The six issuance APIs | ||
|
|
||
| STS is not one function. These are the operations that hand back credentials. |
There was a problem hiding this comment.
"The six issuance APIs" and "STS is not one function. These are the operations that hand back credentials" disagree with the rest of the repo. The README and docs/03-api-operations.md count 11 operations. This page itself later documents GetWebIdentityToken, and the table leaves out the other credential-returning operations. Retitle it as the classic or inbound subset, or reconcile it with the main notes. The same "six issuance APIs" label is in the README.md diagram, and in 03-federation.ja.md:7 ("6つの発行 API").
There was a problem hiding this comment.
Fixed: retitled to the six classic issuance APIs, with a pointer to the full list of 11 operations in docs/03-api-operations.md; the README diagram label is now just 'issuance APIs'.
|
|
||
| ## 6つの発行 API | ||
|
|
||
| STS は単一の関数ではない。クレデンシャルを返すのは次の操作。 |
There was a problem hiding this comment.
Register mismatch. The merged-in JA notes are written in plain form ("STS は単一の関数ではない。", "与えない。"), while the rest of the repo's JA (README.ja.md, the site strings, and the JA signing README line 5, which uses です/ます) is です/ます. Pick one for the chapter's notes. This probably applies to all of docs/signing/*.ja.md.
There was a problem hiding this comment.
The merged notes are their own document set and keep the plain form they were written in (the rest of docs/ is English only); I switched the one added line in docs/signing/README.ja.md to plain form so the file is consistent. The site strings stay です/ます.
| nonce, so signing the same request twice here gives identical bytes. SDK signers typically | ||
| use a randomised nonce and will not (unverified per SDK). Both verify; only one is | ||
| reproducible, which matters if you ever try to write a fixed test vector for a SigV4a | ||
| signature. |
There was a problem hiding this comment.
"SDK signers typically use a randomised nonce and will not (unverified per SDK)" is a claim recalled rather than sourced, and the hedge says so. The same sentence is in 05-verification.md ("SDK signers typically are not, though that is unverified per SDK"). Check at least one SDK, or drop the claim and keep only the part you tested: @noble/curves is deterministic here.
There was a problem hiding this comment.
Dropped the SDK claim. The notes, lab copy and test comments now only say what was tested: @noble/curves is deterministic here, ECDSA permits a random nonce, and either verifies.
…op the unsourced SDK nonce claim
| | `SecretAccessKey` | **Never** | Used locally as HMAC input only | | ||
|
|
||
| That asymmetry is the difference between a signature and a bearer token. Intercepting a | ||
| signed request gives you that one request, already spent. Intercepting a bearer token |
There was a problem hiding this comment.
"Already spent" overstates it. SigV4 carries no nonce, so a captured signed request can be replayed unchanged until the timestamp window closes. The chapter says this itself under "What the signature covers" ("The timestamp bounds replay"). Suggest: "gives you that one request, replayable only inside the clock-skew window and only for the same method, path and body."
The JA text at 02-sigv4.ja.md ("既に使い終わったその1本だけ") needs the same fix.
There was a problem hiding this comment.
Fixed with your wording, EN and JA.
| このリポジトリは `@noble/curves` を使っている。 | ||
|
|
||
| 1つ書いておく価値のある帰結。`@noble/curves` は RFC 6979 の決定的 nonce が既定なので、ここでは同じ | ||
| リクエストに2回署名すると同じバイト列になる。これはこの実装の性質で、SigV4a の性質ではない。ECDSA は乱数の nonce も許すので、そういう署名処理なら毎回違うバイト列になり、それでも同じように検証は通る。どちらも |
There was a problem hiding this comment.
The JA has drifted from the EN. The last sentence of the paragraph, "どちらも検証は通り、再現するのは一方だけ", has no counterpart in 04-variants.md, and it repeats the sentence before it. The line also breaks the file's ~100-column wrapping. Drop the extra sentence and rewrap, so the paragraph ends at "ここが効いてくる。" with the EN's claim and nothing more.
There was a problem hiding this comment.
Fixed: dropped the extra sentence and rewrapped; the JA paragraph now ends where the EN does.
| | `AssumeRole` | An existing signed credential | Role's `MaxSessionDuration` | | ||
| | `AssumeRoleWithSAML` | A SAML assertion, **no AWS credential** | Same | | ||
| | `AssumeRoleWithWebIdentity` | An OIDC ID token, **no AWS credential** | Same | | ||
| | `AssumeRoot` | Management account | Fixed 15 minutes | |
There was a problem hiding this comment.
Two things in the AssumeRoot row read as recalled rather than sourced. "Fixed 15 minutes" should be stated as a maximum, with a link to the API reference, because DurationSeconds is a parameter with an upper bound. "Management account" is also incomplete, since a delegated administrator for centralized root access can call it too. The page cites no source for either.
|
Review of the Signing chapter (docs, EN/JA parity, claims). CI-covered checks were not repeated. Findings (inline):
Checked and found consistent: the SigV4a KDF steps, the presign expiry limits, the GetSessionToken/GetFederationToken ceilings, the Not verifiable offline: the dated STS announcements (January 2026 claim validation, September 2026 4,096-byte token size). They link to AWS pages, so they are worth a manual click-through. |
|
|
||
| # Signing notes | ||
|
|
||
| These notes back the [Signing chapter](https://0-draft.github.io/aws-credentials/#signing). They were written for the former `caller-identity` project and moved here when it was merged into this one on 2026-11-08. |
There was a problem hiding this comment.
The link points at 0-draft.github.io/aws-credentials/, which is not this site. This repo is served at https://0-draft.github.io/assume-role/ (see README.md). The link will 404 or land on the wrong project. README.ja.md:5 has the same URL with ?lang=ja.
| import { SigV4aLab } from "./SigV4aLab"; | ||
| import { Wire } from "./Wire"; | ||
|
|
||
| const REPO = "https://github.com/0-draft/aws-credentials/blob/main"; |
There was a problem hiding this comment.
REPO points at 0-draft/aws-credentials, not 0-draft/assume-role. Every "source / notes" link the section builds from it will point to the wrong repository, or to a path that does not exist, since the notes now live in docs/signing/ here.
|
Found one problem, a wrong repo/site URL, in three places: The new paths, trust and why copy otherwise reads as correct and sourced, and EN/JA parity holds in the parts I read. I did not re-verify the Glacier |
Merges the unique parts of 0-draft/caller-identity into this site, ahead of renaming the repository to
aws-credentials(credential issuance plus signing).What moves in
src/features/signing/sigv4.ts/sigv4a.tsand their tests, unchanged: the WebCrypto SigV4 signer asserted against the AWS Glacier and S3 presigned examples, and the SigV4a P-256 derivation.#signing), EN/JA:AssumeRolecall to STS, temporary credentials to S3) showing the canonical request, string to sign, key derivation and signature, with a "break it" panel and the unsigned-session-token toggle;CreateAccessKey(whereAKIA…comes from, two keys per user);docs/signing/(EN and JA).Deduplication
caller-identity's AssumeRole, GitHub OIDC and EC2 IMDSv2 paths overlap the existing paths here, which are more complete, so those stay. Facts only caller-identity had are folded in: why IMDSv2 uses a PUT with a custom header (SSRF),
TokenvsAWS_SESSION_TOKEN, how the runner requests the ID token (&audience=,ACTIONS_ID_TOKEN_REQUEST_TOKEN, defaultaud), the absentAuthorizationheader onAssumeRoleWithWebIdentity, JWKS, and the echoedSubjectFromWebIdentityToken/Provider. Its trust policy lab overlaps the OIDC lab here; its one extra case (the wrongAction) becomes a callout in the trust chapter. The staleAWS_SESSION_TOKENpitfall moves to the Signing chapter.The README and the "Why temporary" chapter no longer defer signing to caller-identity.
Verification
npm run checklocally: typecheck, ESLint, Prettier, markdownlint, Vitest (34 files, 284 tests, including the SigV4 vectors and new tests for the chapter and its demo data) and build all pass. Playwright could not launch a browser in my local environment, so the E2E and axe checks are left to CI.