import {crypto} from "jsr:@std/crypto"; import {decodeBase64} from "jsr:@std/encoding/base64"; import {encodeBase64Url} from "jsr:@std/encoding/base64url"; import {decodeHex, encodeHex} from "jsr:@std/encoding/hex"; // Reference: // * https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig // * https://docs.deno.com/examples/hex_base64_encoding/ // * https://jsr.io/@std/encoding/1.0.6/base64url.ts // * https://docs.deno.com/examples/hashing/ // * https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey // * https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/verify // #define MAGIC_PREAMBLE "SSHSIG" // #define SIG_VERSION 0x01 // // byte[6] MAGIC_PREAMBLE // uint32 SIG_VERSION // string publickey // @see public key format // string namespace // string reserved // always empty // string hash_algorithm // string signature // @see signature format // // public key format // string ecdsa-sha2-nistp256|384|521 // string nistp256|384|521 // string ec public key point // // signature format // string ecdsa-sha2-nistp256|384|521 // string signature of ssh sign data // @see ec signature format // // ec signature format // string r // string s // // ssh sign data // byte[6] MAGIC_PREAMBLE // string namespace // string reserved // always empty // string hash_algorithm // string input digest hash with @hash_algorithm const ENCODING_UTF8 = "UTF-8"; class BinaryWriter { buffers: Array; constructor() { this.buffers = []; } merge(): Uint8Array { let totalLen = 0; for (let i = 0; i < this.buffers.length; i++) { totalLen += this.buffers[i].byteLength; } let merged = new Uint8Array(totalLen); let offset = 0; for (let i = 0; i < this.buffers.length; i++) { merged.set(this.buffers[i], offset); offset += this.buffers[i].byteLength; } return merged; } writeString(byte: Uint8Array) { this.writeUint32(byte.byteLength); this.writeBytes(byte); } writeStringFromString(str: string) { const bytes = new TextEncoder().encode(str); this.writeString(bytes); } writeLengthFromString(str: string) { const bytes = new TextEncoder().encode(str); this.writeBytes(bytes); } writeUint32(num: number) { let dataView = new DataView(new ArrayBuffer(4), 0); dataView.setUint32(0, num); this.writeBytes(new Uint8Array(dataView.buffer)); } writeNumber(num: number) { let n = new Uint8Array(1) n[0] = num; this.writeBytes(n); } writeBytes(bytes: Uint8Array) { this.buffers.push(bytes); } } class BinaryReader { buffer: Uint8Array; pos: number; constructor(buffer: Uint8Array) { this.buffer = buffer; this.pos = 0; } readStringToString(): string { const buff = this.readString(); return new TextDecoder(ENCODING_UTF8).decode(buff); } readLengthToString(len: number): string { const buff = this._readBytes(len); return new TextDecoder(ENCODING_UTF8).decode(buff); } readString(): Uint8Array { const len = this.readUint32(); return this._readBytes(len); } readUint32(): number { const num = this._readBytes(4); const dataView = new DataView(num.buffer, 0); return dataView.getUint32(0); } _readBytes(len: number): Uint8Array { if (this.buffer.byteLength < (this.pos + len)) { throw `Bad length ${this.buffer.byteLength} < ${this.pos} + ${len}`; } const bytes = this.buffer.slice(this.pos, this.pos + len); this.pos += len; return bytes; } } class SshSignature { publicKey: SshPublicKey; namespace: string; hashAlgorithm: string; signature: SshSignatureValue; constructor(publicKey: SshPublicKey, namespace: string, hashAlgorithm: string, signature: SshSignatureValue) { this.publicKey = publicKey; this.namespace = namespace; this.hashAlgorithm = hashAlgorithm; this.signature = signature; } static parse(buffer: Uint8Array): SshSignature { let reader = new BinaryReader(buffer); const sshSig = reader.readLengthToString(6); if (sshSig !== "SSHSIG") { throw `Bad SSH signature magic, expect: SSHSIG, actual: ${sshSig}`; } let sshVer = reader.readUint32(); if (sshVer !== 1) { throw `Bad SSH signature version, expect: 1, actual: ${sshVer}`; } const publicKeyBytes = reader.readString(); const publicKey = SshPublicKey.parse(publicKeyBytes); const namespace = reader.readStringToString(); const reserved = reader.readStringToString(); if (reserved !== "") { throw `Bad SSH signature reserved value, expect empty, actual: ${reserved}`; } const hashAlgorithm = reader.readStringToString(); const signatureBytes = reader.readString(); const signature = SshSignatureValue.parse(signatureBytes); return new SshSignature(publicKey, namespace, hashAlgorithm, signature); } async calculateSignatureData(filename: string): Promise { const digest = await this._digestFile(filename); let writer = new BinaryWriter(); writer.writeLengthFromString("SSHSIG"); writer.writeStringFromString(this.namespace); writer.writeStringFromString(""); writer.writeStringFromString(this.hashAlgorithm); writer.writeString(digest); return writer.merge(); } async _digestFile(filename: string): Promise { return await digestFile(filename, this.hashAlgorithm); } } class SshSignatureValue { signatureAlgorithm: string; ecSignatureR: Uint8Array; ecSignatureS: Uint8Array; constructor(signatureAlgorithm: string, ecSignatureR: Uint8Array, ecSignatureS: Uint8Array) { this.signatureAlgorithm = signatureAlgorithm; this.ecSignatureR = ecSignatureR; this.ecSignatureS = ecSignatureS; } static parse(buffer: Uint8Array): SshSignatureValue { let reader = new BinaryReader(buffer); const signatureAlgorithm = reader.readStringToString(); const signatureEc = reader.readString(); let signatureEcReader = new BinaryReader(signatureEc); const r = signatureEcReader.readString(); const s = signatureEcReader.readString(); return new SshSignatureValue(signatureAlgorithm, r, s); } // SEQUENCE { // INTEGER: 00cdf3f5e083974961a9737daa2352cf08f7e652f103d4e3cc494b5ffadccf1a6d // INTEGER: 69658b75c9c7523c15e6de16907350f0d0fd51114237c3e32bdd9fe92465e768 // } toDer(): Uint8Array { let writer = new BinaryWriter(); writer.writeNumber(0x30); writer.writeNumber(0x45); writer.writeNumber(2); const rFirstByte = this.ecSignatureR[0]; writer.writeNumber(((rFirstByte >= 0x80) ? 1 : 0) + this.ecSignatureR.byteLength); if (rFirstByte >= 0x80) { writer.writeNumber(0); } writer.writeBytes(this.ecSignatureR); writer.writeNumber(2); const sFirstByte = this.ecSignatureS[0]; writer.writeNumber(((sFirstByte >= 0x80) ? 1 : 0) + this.ecSignatureS.byteLength); if (sFirstByte >= 0x80) { writer.writeNumber(0); } writer.writeBytes(this.ecSignatureS); return writer.merge(); } } class SshPublicKey { signatureAlgorithm: string; algorithm: string; publicKeyPoint: Uint8Array; constructor(signatureAlgorithm: string, algorithm: string, publicKeyPoint: Uint8Array) { this.signatureAlgorithm = signatureAlgorithm; this.algorithm = algorithm; this.publicKeyPoint = publicKeyPoint; } static parse(buffer: Uint8Array): SshPublicKey { let reader = new BinaryReader(buffer); const signatureAlgorithm = reader.readStringToString(); const curveAlgorithm = reader.readStringToString(); const publicKeyPoint = reader.readString(); if (signatureAlgorithm !== `ecdsa-sha2-${curveAlgorithm}`) { throw `Not supported signature algorithm ${signatureAlgorithm} or curve algorithm ${curveAlgorithm}`; } return new SshPublicKey(signatureAlgorithm, curveAlgorithm, publicKeyPoint); } async importJwk(): Promise { let jwk = this.toJwk(); return await crypto.subtle.importKey( "jwk", jwk, { name: "ECDSA", namedCurve: (this.algorithm === "nistp256") ? "P-256" : "P-384", }, true, ["verify"], ); } toJwk(): any { if (this.publicKeyPoint[0] !== 0x04) { throw `Invalid EC public key point: ${encodeHex(this.publicKeyPoint)}`; } let coordinateLength; if (this.algorithm === "nistp256") { coordinateLength = 256 / 8; } else if (this.algorithm === "nistp384") { coordinateLength = 384 / 8; } else { throw `Not supported alrogithm: ${this.algorithm}`; } const x = this.publicKeyPoint.slice(1, coordinateLength + 1); const y = this.publicKeyPoint.slice(coordinateLength + 1, coordinateLength + coordinateLength + 1); return { crv: (this.algorithm === "nistp256") ? "P-256" : "P-384", ext: true, key_ops: ["verify"], kty: "EC", x: encodeBase64Url(x), y: encodeBase64Url(y), }; } } function parsePem(pem: string): Uint8Array { const lines = pem.split("\n"); let isInPem = false; let innerPem = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (isInPem) { if (line.indexOf("-----END ") === 0) { isInPem = false; break; } else { innerPem.push(line); } } else if (line.indexOf("-----BEGIN ") === 0) { isInPem = true; } } if (innerPem.length === 0) { return null; } return decodeBase64(innerPem.join("")); } async function digestFile(filename: string, algorithm: string): Promise { let hashAlgorithm; switch (algorithm.toLowerCase()) { case "sha256": case "sha-256": hashAlgorithm = "SHA-256"; break; case "sha384": case "sha-384": hashAlgorithm = "SHA-384"; break; case "sha512": case "sha-512": hashAlgorithm = "SHA-512"; break; 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); return new Uint8Array(fileHashBuffer); } const TEST_SIG = `-----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1 NgAAAEEE3T7r2QbJzwCwjsKfftYYBNHMHRNS2SV7YoGR4I/DcXxPrjKYzVxIKc7I vzqUbn22C3hX4Sh/aguuaz8jQvAH0AAAAARmaWxlAAAAAAAAAAZzaGE1MTIAAABk AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAABJAAAAIQDD7MAiklQEsY3Dq3Zy25Zz 9y4mNmPAWlbQPAAg2zyc1wAAACAdQZcn+tnINX8PRt3mHTxpE2+8I/ioTPWHhne1 0oJ+4w== -----END SSH SIGNATURE-----`; const sshSignature = SshSignature.parse(parsePem(TEST_SIG)); console.log(sshSignature); const file = "/Users/hatterjiang/temp/sigstore-tests/hello.txt"; const sig = await sshSignature.calculateSignatureData(file); console.log(encodeHex(sig)); const publicKey = await sshSignature.publicKey.importJwk(); console.log(publicKey); console.log(encodeHex(sshSignature.signature.toDer())); // crypto.subtle.verify( // { // name: "ECDSA", // hash: { name: "SHA-256" }, // }, // publicKey, // signature, // encoded, // );