feat: add deno-sshsig-mod.ts
This commit is contained in:
344
libraries/deno-sshsig-mod.ts
Normal file
344
libraries/deno-sshsig-mod.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
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";
|
||||
|
||||
// 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<Uint8Array>;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
_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<Uint8Array> {
|
||||
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<Uint8Array> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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<CryptoKey> {
|
||||
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<Uint8Array> {
|
||||
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);
|
||||
|
||||
// crypto.subtle.verify(
|
||||
// {
|
||||
// name: "ECDSA",
|
||||
// hash: { name: "SHA-256" },
|
||||
// },
|
||||
// publicKey,
|
||||
// signature,
|
||||
// encoded,
|
||||
// );
|
||||
|
||||
Reference in New Issue
Block a user