@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.
- package/README.md +51 -34
- package/hipp.js +32 -24
- 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
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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.
|
|
179
|
-
"hash": "
|
|
180
|
-
"signature": "
|
|
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
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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
|
-
|
|
599
|
-
const
|
|
600
|
-
|
|
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
|
-
|
|
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:
|
|
622
|
+
hash: tarballHash,
|
|
619
623
|
signature: signature,
|
|
620
624
|
};
|
|
621
625
|
|
|
622
|
-
stagedReadme
|
|
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...');
|