diff --git a/libraries/deno-sshsig-mod.ts b/libraries/deno-sshsig-mod.ts index 44527f9..e4c4fab 100644 --- a/libraries/deno-sshsig-mod.ts +++ b/libraries/deno-sshsig-mod.ts @@ -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 { + 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 { + async verifyString(data: string): Promise { + const digest = await this._digestString(data); + return await this.verifyDigest(digest); + } + + async verifyFile(filename: string): Promise { const digest = await this._digestFile(filename); + return await this.verifyDigest(digest); + } + + async verifyDigest(digest: Uint8Array): Promise { + 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 { + return await digestString(data, this.hashAlgorithm); } async _digestFile(filename: string): Promise { @@ -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 { 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 { + 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 { + 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