feat: ssh sign verify works now
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import {crypto} from "jsr:@std/crypto";
|
import {crypto} from "jsr:@std/crypto";
|
||||||
import {decodeBase64} from "jsr:@std/encoding/base64";
|
import {decodeBase64} from "jsr:@std/encoding/base64";
|
||||||
import {encodeBase64Url} from "jsr:@std/encoding/base64url";
|
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:
|
// Reference:
|
||||||
// * https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig
|
// * https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig
|
||||||
@@ -51,7 +53,7 @@ class BinaryWriter {
|
|||||||
this.buffers = [];
|
this.buffers = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
merge(): Uint8Array {
|
toArray(): Uint8Array {
|
||||||
let totalLen = 0;
|
let totalLen = 0;
|
||||||
for (let i = 0; i < this.buffers.length; i++) {
|
for (let i = 0; i < this.buffers.length; i++) {
|
||||||
totalLen += this.buffers[i].byteLength;
|
totalLen += this.buffers[i].byteLength;
|
||||||
@@ -112,22 +114,22 @@ class BinaryReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readLengthToString(len: number): string {
|
readLengthToString(len: number): string {
|
||||||
const buff = this._readBytes(len);
|
const buff = this.readBytes(len);
|
||||||
return new TextDecoder(ENCODING_UTF8).decode(buff);
|
return new TextDecoder(ENCODING_UTF8).decode(buff);
|
||||||
}
|
}
|
||||||
|
|
||||||
readString(): Uint8Array {
|
readString(): Uint8Array {
|
||||||
const len = this.readUint32();
|
const len = this.readUint32();
|
||||||
return this._readBytes(len);
|
return this.readBytes(len);
|
||||||
}
|
}
|
||||||
|
|
||||||
readUint32(): number {
|
readUint32(): number {
|
||||||
const num = this._readBytes(4);
|
const num = this.readBytes(4);
|
||||||
const dataView = new DataView(num.buffer, 0);
|
const dataView = new DataView(num.buffer, 0);
|
||||||
return dataView.getUint32(0);
|
return dataView.getUint32(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
_readBytes(len: number): Uint8Array {
|
readBytes(len: number): Uint8Array {
|
||||||
if (this.buffer.byteLength < (this.pos + len)) {
|
if (this.buffer.byteLength < (this.pos + len)) {
|
||||||
throw `Bad length ${this.buffer.byteLength} < ${this.pos} + ${len}`;
|
throw `Bad length ${this.buffer.byteLength} < ${this.pos} + ${len}`;
|
||||||
}
|
}
|
||||||
@@ -150,6 +152,16 @@ class SshSignature {
|
|||||||
this.signature = signature;
|
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 {
|
static parse(buffer: Uint8Array): SshSignature {
|
||||||
let reader = new BinaryReader(buffer);
|
let reader = new BinaryReader(buffer);
|
||||||
const sshSig = reader.readLengthToString(6);
|
const sshSig = reader.readLengthToString(6);
|
||||||
@@ -173,15 +185,62 @@ class SshSignature {
|
|||||||
return new SshSignature(publicKey, namespace, hashAlgorithm, signature);
|
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);
|
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();
|
let writer = new BinaryWriter();
|
||||||
writer.writeLengthFromString("SSHSIG");
|
writer.writeLengthFromString("SSHSIG");
|
||||||
writer.writeStringFromString(this.namespace);
|
writer.writeStringFromString(this.namespace);
|
||||||
writer.writeStringFromString("");
|
writer.writeStringFromString("");
|
||||||
writer.writeStringFromString(this.hashAlgorithm);
|
writer.writeStringFromString(this.hashAlgorithm);
|
||||||
writer.writeString(digest);
|
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> {
|
async _digestFile(filename: string): Promise<Uint8Array> {
|
||||||
@@ -210,6 +269,22 @@ class SshSignatureValue {
|
|||||||
return new SshSignatureValue(signatureAlgorithm, r, s);
|
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 {
|
// SEQUENCE {
|
||||||
// INTEGER: 00cdf3f5e083974961a9737daa2352cf08f7e652f103d4e3cc494b5ffadccf1a6d
|
// INTEGER: 00cdf3f5e083974961a9737daa2352cf08f7e652f103d4e3cc494b5ffadccf1a6d
|
||||||
// INTEGER: 69658b75c9c7523c15e6de16907350f0d0fd51114237c3e32bdd9fe92465e768
|
// INTEGER: 69658b75c9c7523c15e6de16907350f0d0fd51114237c3e32bdd9fe92465e768
|
||||||
@@ -234,7 +309,7 @@ class SshSignatureValue {
|
|||||||
writer.writeNumber(0);
|
writer.writeNumber(0);
|
||||||
}
|
}
|
||||||
writer.writeBytes(this.ecSignatureS);
|
writer.writeBytes(this.ecSignatureS);
|
||||||
return writer.merge();
|
return writer.toArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,6 +335,21 @@ class SshPublicKey {
|
|||||||
return new SshPublicKey(signatureAlgorithm, curveAlgorithm, publicKeyPoint);
|
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> {
|
async importJwk(): Promise<CryptoKey> {
|
||||||
let jwk = this.toJwk();
|
let jwk = this.toJwk();
|
||||||
return await crypto.subtle.importKey(
|
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");
|
const lines = pem.split("\n");
|
||||||
let isInPem = false;
|
let isInPem = false;
|
||||||
let innerPem = [];
|
let innerPem = [];
|
||||||
@@ -322,7 +412,22 @@ function parsePem(pem: string): Uint8Array {
|
|||||||
return decodeBase64(innerPem.join(""));
|
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> {
|
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;
|
let hashAlgorithm;
|
||||||
switch (algorithm.toLowerCase()) {
|
switch (algorithm.toLowerCase()) {
|
||||||
case "sha256":
|
case "sha256":
|
||||||
@@ -340,44 +445,22 @@ async function digestFile(filename: string, algorithm: string): Promise<Uint8Arr
|
|||||||
default:
|
default:
|
||||||
throw `Unknown algorithm: ${algorithm}`;
|
throw `Unknown algorithm: ${algorithm}`;
|
||||||
}
|
}
|
||||||
const file = await Deno.open(filename, {read: true});
|
return hashAlgorithm;
|
||||||
const readableStream = file.readable;
|
|
||||||
const fileHashBuffer = await crypto.subtle.digest(hashAlgorithm, readableStream);
|
|
||||||
console.log("XXXXX", encodeHex(fileHashBuffer));
|
|
||||||
return new Uint8Array(fileHashBuffer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TEST_SIG = `-----BEGIN SSH SIGNATURE-----
|
async function testSshSig() {
|
||||||
|
const TEST_SIG = `-----BEGIN SSH SIGNATURE-----
|
||||||
U1NIU0lHAAAAAQAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1
|
U1NIU0lHAAAAAQAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1
|
||||||
NgAAAEEE3T7r2QbJzwCwjsKfftYYBNHMHRNS2SV7YoGR4I/DcXxPrjKYzVxIKc7I
|
NgAAAEEE3T7r2QbJzwCwjsKfftYYBNHMHRNS2SV7YoGR4I/DcXxPrjKYzVxIKc7I
|
||||||
vzqUbn22C3hX4Sh/aguuaz8jQvAH0AAAAARmaWxlAAAAAAAAAAZzaGE1MTIAAABk
|
vzqUbn22C3hX4Sh/aguuaz8jQvAH0AAAAARmaWxlAAAAAAAAAAZzaGE1MTIAAABk
|
||||||
AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAABJAAAAIQDD7MAiklQEsY3Dq3Zy25Zz
|
AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAABJAAAAIQDsOkCrkDFdGUeKYJ6smchz
|
||||||
9y4mNmPAWlbQPAAg2zyc1wAAACAdQZcn+tnINX8PRt3mHTxpE2+8I/ioTPWHhne1
|
qE04Ba+c5O/vN5Lld3pjFgAAACBZt+dnGiirCy/3XjKcfQYY8drtmam1QaTelyHV
|
||||||
0oJ+4w==
|
WRxFFw==
|
||||||
-----END SSH SIGNATURE-----`;
|
-----END SSH SIGNATURE-----`;
|
||||||
|
|
||||||
const sshSignature = SshSignature.parse(parsePem(TEST_SIG));
|
const sshSignature = SshSignature.parsePem(TEST_SIG);
|
||||||
console.log(sshSignature);
|
const data = new TextDecoder(ENCODING_UTF8).decode(decodeBase64("aGVsbG8gaGF0dGVyIDIwMjUK"));
|
||||||
|
console.log(await sshSignature.verifyString(data));
|
||||||
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);
|
|
||||||
|
|
||||||
|
// await testSshSig();
|
||||||
|
|||||||
Reference in New Issue
Block a user