feat: ssh sign verify works now

This commit is contained in:
2025-01-11 23:26:25 +08:00
parent 85dbc76755
commit 5b48c13727

View File

@@ -1,7 +1,9 @@
import {crypto} from "jsr:@std/crypto";
import {decodeBase64} from "jsr:@std/encoding/base64";
import {encodeBase64Url} from "jsr:@std/encoding/base64url";
import {encodeHex} from "jsr:@std/encoding/hex";
import {decodeHex, encodeHex} from "jsr:@std/encoding/hex";
// IMPORTANT: ONLY supports ECDSA P256 and P384
// Reference:
// * https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig
@@ -51,7 +53,7 @@ class BinaryWriter {
this.buffers = [];
}
merge(): Uint8Array {
toArray(): Uint8Array {
let totalLen = 0;
for (let i = 0; i < this.buffers.length; i++) {
totalLen += this.buffers[i].byteLength;
@@ -112,22 +114,22 @@ class BinaryReader {
}
readLengthToString(len: number): string {
const buff = this._readBytes(len);
const buff = this.readBytes(len);
return new TextDecoder(ENCODING_UTF8).decode(buff);
}
readString(): Uint8Array {
const len = this.readUint32();
return this._readBytes(len);
return this.readBytes(len);
}
readUint32(): number {
const num = this._readBytes(4);
const num = this.readBytes(4);
const dataView = new DataView(num.buffer, 0);
return dataView.getUint32(0);
}
_readBytes(len: number): Uint8Array {
readBytes(len: number): Uint8Array {
if (this.buffer.byteLength < (this.pos + len)) {
throw `Bad length ${this.buffer.byteLength} < ${this.pos} + ${len}`;
}
@@ -150,6 +152,16 @@ class SshSignature {
this.signature = signature;
}
static async parseFile(filename: string): Promise<SshSignature> {
const pem = await Deno.readTextFile(filename);
return SshSignature.parsePem(pem);
}
static parsePem(pem: string): SshSignature {
const buffer = parsePemToArray(pem);
return SshSignature.parse(buffer);
}
static parse(buffer: Uint8Array): SshSignature {
let reader = new BinaryReader(buffer);
const sshSig = reader.readLengthToString(6);
@@ -173,15 +185,62 @@ class SshSignature {
return new SshSignature(publicKey, namespace, hashAlgorithm, signature);
}
async calculateSignatureData(filename: string): Promise<Uint8Array> {
async verifyString(data: string): Promise<boolean> {
const digest = await this._digestString(data);
return await this.verifyDigest(digest);
}
async verifyFile(filename: string): Promise<boolean> {
const digest = await this._digestFile(filename);
return await this.verifyDigest(digest);
}
async verifyDigest(digest: Uint8Array): Promise<boolean> {
const publicKey = await this.publicKey.importJwk();
const signature = this.signature.toRs();
const signatureData = this.calculateSignatureData(digest);
return await crypto.subtle.verify(
{
name: "ECDSA",
hash: {name: (this.publicKey.algorithm === "nistp256") ? "SHA-256" : "SHA-384"},
},
publicKey,
signature,
signatureData,
);
}
calculateSignatureData(digest: Uint8Array): Uint8Array {
if (this.getHashAlgorithmLength() !== digest.byteLength) {
throw `Bad digest length, expect: ${this.getHashAlgorithmLength()}, acutal: ${digest.byteLength}`;
}
let writer = new BinaryWriter();
writer.writeLengthFromString("SSHSIG");
writer.writeStringFromString(this.namespace);
writer.writeStringFromString("");
writer.writeStringFromString(this.hashAlgorithm);
writer.writeString(digest);
return writer.merge();
return writer.toArray();
}
getHashAlgorithmLength(): number {
switch (this.hashAlgorithm.toLowerCase()) {
case "sha512":
case "sha-512":
return 512 / 8;
case "sha384":
case "sha-384":
return 384 / 8;
case "sha256":
case "sha-256":
return 256 / 8;
default:
throw `Unknown hash algorithm: ${this.hashAlgorithm}`;
}
}
async _digestString(data: string): Promise<Uint8Array> {
return await digestString(data, this.hashAlgorithm);
}
async _digestFile(filename: string): Promise<Uint8Array> {
@@ -210,6 +269,22 @@ class SshSignatureValue {
return new SshSignatureValue(signatureAlgorithm, r, s);
}
// crypto.subtle.sign's signature is R and S
toRs(): Uint8Array {
let writer = new BinaryWriter();
if (this.ecSignatureR.byteLength === 0x21) {
writer.writeBytes(this.ecSignatureR.slice(1));
} else {
writer.writeBytes(this.ecSignatureR);
}
if (this.ecSignatureS.byteLength === 0x21) {
writer.writeBytes(this.ecSignatureS.slice(1));
} else {
writer.writeBytes(this.ecSignatureS);
}
return writer.toArray();
}
// SEQUENCE {
// INTEGER: 00cdf3f5e083974961a9737daa2352cf08f7e652f103d4e3cc494b5ffadccf1a6d
// INTEGER: 69658b75c9c7523c15e6de16907350f0d0fd51114237c3e32bdd9fe92465e768
@@ -234,7 +309,7 @@ class SshSignatureValue {
writer.writeNumber(0);
}
writer.writeBytes(this.ecSignatureS);
return writer.merge();
return writer.toArray();
}
}
@@ -260,6 +335,21 @@ class SshPublicKey {
return new SshPublicKey(signatureAlgorithm, curveAlgorithm, publicKeyPoint);
}
toDer(): Uint8Array {
let writer = new BinaryWriter();
if (this.algorithm === "nistp256") {
writer.writeBytes(decodeHex("3059301306072a8648ce3d020106082a8648ce3d030107034200"));
} else {
writer.writeBytes(decodeHex("3076301006072a8648ce3d020106052b81040022036200"));
}
writer.writeBytes(this.asPoint());
return writer.toArray();
}
asPoint(): Uint8Array {
return this.publicKeyPoint;
}
async importJwk(): Promise<CryptoKey> {
let jwk = this.toJwk();
return await crypto.subtle.importKey(
@@ -299,7 +389,7 @@ class SshPublicKey {
}
}
function parsePem(pem: string): Uint8Array {
function parsePemToArray(pem: string): Uint8Array {
const lines = pem.split("\n");
let isInPem = false;
let innerPem = [];
@@ -322,7 +412,22 @@ function parsePem(pem: string): Uint8Array {
return decodeBase64(innerPem.join(""));
}
async function digestString(data: string, algorithm: string): Promise<Uint8Array> {
let hashAlgorithm = normalizeHashAlgorithm(algorithm);
const messageBuffer = new TextEncoder().encode(data);
const hashBuffer = await crypto.subtle.digest(hashAlgorithm, messageBuffer);
return new Uint8Array(hashBuffer);
}
async function digestFile(filename: string, algorithm: string): Promise<Uint8Array> {
let hashAlgorithm = normalizeHashAlgorithm(algorithm);
const file = await Deno.open(filename, {read: true});
const readableStream = file.readable;
const hashBuffer = await crypto.subtle.digest(hashAlgorithm, readableStream);
return new Uint8Array(hashBuffer);
}
function normalizeHashAlgorithm(algorithm: string): string {
let hashAlgorithm;
switch (algorithm.toLowerCase()) {
case "sha256":
@@ -340,44 +445,22 @@ async function digestFile(filename: string, algorithm: string): Promise<Uint8Arr
default:
throw `Unknown algorithm: ${algorithm}`;
}
const file = await Deno.open(filename, {read: true});
const readableStream = file.readable;
const fileHashBuffer = await crypto.subtle.digest(hashAlgorithm, readableStream);
console.log("XXXXX", encodeHex(fileHashBuffer));
return new Uint8Array(fileHashBuffer);
return hashAlgorithm;
}
const TEST_SIG = `-----BEGIN SSH SIGNATURE-----
async function testSshSig() {
const TEST_SIG = `-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1
NgAAAEEE3T7r2QbJzwCwjsKfftYYBNHMHRNS2SV7YoGR4I/DcXxPrjKYzVxIKc7I
vzqUbn22C3hX4Sh/aguuaz8jQvAH0AAAAARmaWxlAAAAAAAAAAZzaGE1MTIAAABk
AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAABJAAAAIQDD7MAiklQEsY3Dq3Zy25Zz
9y4mNmPAWlbQPAAg2zyc1wAAACAdQZcn+tnINX8PRt3mHTxpE2+8I/ioTPWHhne1
0oJ+4w==
AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAABJAAAAIQDsOkCrkDFdGUeKYJ6smchz
qE04Ba+c5O/vN5Lld3pjFgAAACBZt+dnGiirCy/3XjKcfQYY8drtmam1QaTelyHV
WRxFFw==
-----END SSH SIGNATURE-----`;
const sshSignature = SshSignature.parse(parsePem(TEST_SIG));
console.log(sshSignature);
const publicKey = await sshSignature.publicKey.importJwk();
console.log(publicKey);
console.log("signature:", encodeHex(sshSignature.signature.toDer()));
const signature = sshSignature.signature.toDer();
const file = "/Users/hatterjiang/temp/sigstore-tests/hello.txt";
const encoded = await sshSignature.calculateSignatureData(file);
console.log("encoded:", encodeHex(encoded));
const r: boolean = await crypto.subtle.verify(
{
name: "ECDSA",
hash: {name: "SHA-256"},
},
publicKey,
signature,
encoded,
);
console.log(r);
const sshSignature = SshSignature.parsePem(TEST_SIG);
const data = new TextDecoder(ENCODING_UTF8).decode(decodeBase64("aGVsbG8gaGF0dGVyIDIwMjUK"));
console.log(await sshSignature.verifyString(data));
}
// await testSshSig();