feat: ssh sign verify works now
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user