@dk/hipp 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +51 -34
  2. package/hipp.js +32 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -99,18 +99,58 @@ npx @dk/hipp verify @dk/your-package[@version]
99
99
 
100
100
  ### How Verification Works
101
101
 
102
- 1. **Fetch from npm**: Downloads the package tarball and extracts the manifest
103
- 2. **Fetch from git**: Clones the repository at the tagged commit and extracts the public key
104
- 3. **Hash Verification**: Computes the hash of the git content and compares with the manifest
105
- 4. **Signature Verification**: Verifies the manifest signature using the public key
102
+ **Step 1: Get manifest from npm**
106
103
 
107
- If both checks pass, you can be certain that:
108
- - The package contents in npm exactly match the git tag
109
- - The manifest was signed by the holder of the private key
104
+ 1. Fetch the package tarball from npm registry
105
+ 2. Extract the README from the tarball
106
+ 3. Parse the JSON manifest appended to the README
107
+
108
+ The manifest contains:
109
+ ```json
110
+ {
111
+ "origin": "git@github.com:dk/your-package.git",
112
+ "tag": "v1.0.0",
113
+ "hash": "<sha256-of-tarball>",
114
+ "signature": "<base64-ed25519-signature>"
115
+ }
116
+ ```
117
+
118
+ **Step 2: Clone git and stage**
119
+
120
+ 4. Clone the repository at the tagged commit (using origin/tag from manifest)
121
+ 5. Copy all tracked files to a staging directory
122
+
123
+ **Step 3: Verify content integrity**
124
+
125
+ 6. Run `npm pack` in the staging directory
126
+ 7. Compute the SHA256 hash of the resulting tarball
127
+ 8. Compare this hash with the `hash` field from the npm manifest
128
+
129
+ **If the hashes match**: The npm package exactly matches the git repository at the tagged commit.
130
+
131
+ **Step 4: Verify signature authenticity**
132
+
133
+ 9. Read `hipp.pub` from the cloned repository at the tagged commit
134
+ 10. Verify the signature using the public key
135
+
136
+ The signature was created by signing: `hash + "\n" + origin + "\n" + tag`
137
+
138
+ **If the signature is valid**: The package was published by the holder of the private key matching `hipp.pub`.
139
+
140
+ ### What Verification Guarantees
141
+
142
+ | Check | Guarantees |
143
+ |-------|-----------|
144
+ | **Hash match** | npm package content exactly matches git at the tagged commit |
145
+ | **Signature valid** | Published by holder of the private key matching `hipp.pub` |
146
+
147
+ This provides two independent guarantees:
148
+ - **Integrity**: The code in npm is exactly what was in git at the tag
149
+ - **Authenticity**: The publisher controls the private key for `hipp.pub`
110
150
 
111
151
  ### Integrity Rules
112
152
 
113
- HIPP enforces strict integrity rules:
153
+ HIPP enforces strict integrity rules when publishing:
114
154
 
115
155
  - `package.json` version must be `0.0.0`
116
156
  - `package-lock.json` must exist and be tracked by git
@@ -149,34 +189,11 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
149
189
  OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
150
190
  PERFORMANCE OF THIS SOFTWARE.
151
191
 
152
-
153
- ```json
154
- {
155
- "origin": "git@github.com:dmytri/hipp.git",
156
- "tag": "v0.1.10"
157
- }
158
- ```
159
-
160
- ```npx @dk/hipp @dk/hipp@0.1.10
161
- ```
162
-
163
- <!-- HIPP-META -->
164
- ```json
165
- {
166
- "origin": "git@github.com:dmytri/hipp.git",
167
- "tag": "v0.1.11"
168
- }
169
- ```
170
-
171
- ```npx @dk/hipp @dk/hipp@0.1.11
172
- ```
173
- <!-- /HIPP-META -->
174
-
175
192
  ```json
176
193
  {
177
194
  "origin": "git@github.com:dmytri/hipp.git",
178
- "tag": "v0.1.14",
179
- "hash": "c2a32be7dbb481c7f9e9444b993616bb1e10d41cf9c0b787642873833ebcc77e",
180
- "signature": "RsopqB7L4VWwi1jBmG1dG6L+Zo0ZV7m5k6jfA+KwafViXgWw6KQniJcCs8aqgujarAANd5AzyHBdlWCNoIFzBg=="
195
+ "tag": "v0.1.16",
196
+ "hash": "935d7b90b43afd348840645f7fd629054bf50716c317fc41065b2cccf7513680",
197
+ "signature": "QIM4TZ3kuYv7/z3RZSkVPK74XI1PFyD/XLARR2evDCEwNOt1VlXIPXwaMZFn1kH0gDQxx3PuF73Kokld+dMjBw=="
181
198
  }
182
199
  ```
package/hipp.js CHANGED
@@ -160,12 +160,24 @@ function findLastJsonBlock(readmeContent) {
160
160
  return lastValid;
161
161
  }
162
162
 
163
- function computeReadmeHash(readmeContent) {
164
- const jsonBlock = findLastJsonBlock(readmeContent);
165
- if (!jsonBlock) return sha256(readmeContent);
166
- const jsonStr = JSON.stringify(jsonBlock, null, 2);
167
- const beforeJson = readmeContent.split('```json')[0];
168
- return sha256(beforeJson + '```json\n' + jsonStr + '\n```\n');
163
+ function packAndHash(stageDir) {
164
+ const result = spawnSync('npm', ['pack'], {
165
+ cwd: stageDir,
166
+ encoding: 'utf8',
167
+ stdio: ['pipe', 'pipe', 'pipe'],
168
+ });
169
+
170
+ if (result.status !== 0) {
171
+ throw new Error(`npm pack failed: ${result.stderr}`);
172
+ }
173
+
174
+ const tarballName = result.stdout.trim().split('\n').pop();
175
+ const tarballPath = path.join(stageDir, tarballName);
176
+
177
+ const tarballContent = fs.readFileSync(tarballPath);
178
+ fs.unlinkSync(tarballPath);
179
+
180
+ return { tarballName, tarballHash: sha256(tarballContent) };
169
181
  }
170
182
 
171
183
  function safeStageName(name) {
@@ -484,14 +496,10 @@ async function runVerify(packageSpec) {
484
496
  const trackedFiles = getTrackedFilesFromDir(tmpDir);
485
497
  copyTrackedFilesFromDir(stageDir, tmpDir, trackedFiles);
486
498
 
487
- const stagedReadmePath = path.join(stageDir, 'README.md');
488
- if (!fs.existsSync(stagedReadmePath)) {
489
- fail(`❌ README.md not found in git at tag ${tag}`);
490
- }
499
+ log.info(`📦 Packing to verify content hash...`);
500
+ const { tarballHash } = packAndHash(stageDir);
491
501
 
492
- const stagedHash = sha256(fs.readFileSync(stagedReadmePath, 'utf8'));
493
-
494
- if (stagedHash !== npmHash) {
502
+ if (tarballHash !== npmHash) {
495
503
  fail(`❌ Hash mismatch: git content does not match npm manifest`);
496
504
  }
497
505
 
@@ -595,10 +603,9 @@ async function run() {
595
603
 
596
604
  const { privateKey } = loadOrGenerateKeys();
597
605
 
598
- const stagedPkgPath = path.join(stageDir, 'package.json');
599
- const stagedPkg = JSON.parse(fs.readFileSync(stagedPkgPath, 'utf8'));
600
- stagedPkg.version = version;
601
- fs.writeFileSync(stagedPkgPath, JSON.stringify(stagedPkg, null, 2) + '\n');
606
+ log.info(`📦 Packing to compute content hash...`);
607
+ const { tarballHash } = packAndHash(stageDir);
608
+ log.success(`🔒 Content hash: ${tarballHash.slice(0, 12)}...`);
602
609
 
603
610
  const stagedReadmePath = path.join(stageDir, 'README.md');
604
611
  let stagedReadme = '';
@@ -606,23 +613,24 @@ async function run() {
606
613
  stagedReadme = fs.readFileSync(stagedReadmePath, 'utf8');
607
614
  }
608
615
 
609
- stagedReadme = stagedReadme.trimEnd() + '\n\n';
610
-
611
- const readmeHash = sha256(stagedReadme);
612
- const dataToSign = buildSignData(readmeHash, provenance.remoteUrl, rawTag);
616
+ const dataToSign = buildSignData(tarballHash, provenance.remoteUrl, rawTag);
613
617
  const signature = signContent(dataToSign, privateKey);
614
618
 
615
619
  const manifestJson = {
616
620
  origin: provenance.remoteUrl,
617
621
  tag: rawTag,
618
- hash: readmeHash,
622
+ hash: tarballHash,
619
623
  signature: signature,
620
624
  };
621
625
 
622
- stagedReadme += '```json\n' + JSON.stringify(manifestJson, null, 2) + '\n```\n';
623
-
626
+ stagedReadme = stagedReadme.trimEnd() + '\n\n```json\n' + JSON.stringify(manifestJson, null, 2) + '\n```\n';
624
627
  fs.writeFileSync(stagedReadmePath, stagedReadme);
625
628
 
629
+ const stagedPkgPath = path.join(stageDir, 'package.json');
630
+ const stagedPkg = JSON.parse(fs.readFileSync(stagedPkgPath, 'utf8'));
631
+ stagedPkg.version = version;
632
+ fs.writeFileSync(stagedPkgPath, JSON.stringify(stagedPkg, null, 2) + '\n');
633
+
626
634
  log.success('🔏 Manifest signed.');
627
635
 
628
636
  log.info('🔥 Ignition...');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dk/hipp",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "High Integrity Package Publisher",
5
5
  "main": "hipp.js",
6
6
  "bin": {