|
| 1 | +import assert from 'assert'; |
| 2 | + |
| 3 | +import { decode, encode } from 'cborg'; |
| 4 | + |
| 5 | +import { |
| 6 | + INTEGRITY_BLOCK_MAGIC, |
| 7 | + SIGNATURE_ATTRIBUTE_TO_TYPE_MAPPING, |
| 8 | + VERSION_B2, |
| 9 | + WEB_BUNDLE_ID_ATTRIBUTE_NAME, |
| 10 | + type SignatureType, |
| 11 | +} from '../utils/constants.js'; |
| 12 | + |
| 13 | +export type SignatureAttributes = { |
| 14 | + [SignatureAttributeKey: string]: Uint8Array; |
| 15 | +}; |
| 16 | + |
| 17 | +export type IntegritySignature = { |
| 18 | + signatureAttributes: SignatureAttributes; |
| 19 | + signature: Uint8Array; |
| 20 | +}; |
| 21 | + |
| 22 | +export class IntegrityBlock { |
| 23 | + private attributes: Map<string, string> = new Map(); |
| 24 | + private signatureStack: IntegritySignature[] = []; |
| 25 | + |
| 26 | + /** @internal */ |
| 27 | + constructor() {} |
| 28 | + |
| 29 | + static fromCbor(integrityBlockBytes: Uint8Array): IntegrityBlock { |
| 30 | + const integrityBlock = new IntegrityBlock(); |
| 31 | + try { |
| 32 | + const [magic, version, attributes, signatureStack] = decode( |
| 33 | + integrityBlockBytes, |
| 34 | + { useMaps: true } |
| 35 | + ); |
| 36 | + |
| 37 | + assert(magic instanceof Uint8Array, 'Invalid magic bytes'); |
| 38 | + assert.deepStrictEqual( |
| 39 | + magic, |
| 40 | + INTEGRITY_BLOCK_MAGIC, |
| 41 | + 'Invalid magic bytes' |
| 42 | + ); |
| 43 | + |
| 44 | + assert(version instanceof Uint8Array, 'Invalid version'); |
| 45 | + assert.deepStrictEqual(version, VERSION_B2, 'Invalid version'); |
| 46 | + |
| 47 | + assert(attributes instanceof Map, 'Invalid attributes'); |
| 48 | + assert( |
| 49 | + attributes.has(WEB_BUNDLE_ID_ATTRIBUTE_NAME), |
| 50 | + `Missing ${WEB_BUNDLE_ID_ATTRIBUTE_NAME} attribute` |
| 51 | + ); |
| 52 | + integrityBlock.setWebBundleId( |
| 53 | + attributes.get(WEB_BUNDLE_ID_ATTRIBUTE_NAME)! |
| 54 | + ); |
| 55 | + |
| 56 | + assert(signatureStack instanceof Array, 'Invalid signature stack'); |
| 57 | + assert(signatureStack.length > 0, 'Invalid signature stack'); |
| 58 | + |
| 59 | + for (const signatureBlock of signatureStack) { |
| 60 | + assert(signatureBlock instanceof Array, 'Invalid signature'); |
| 61 | + assert.strictEqual(signatureBlock.length, 2, 'Invalid signature'); |
| 62 | + |
| 63 | + const [attributes, signature] = signatureBlock; |
| 64 | + assert(attributes instanceof Map, 'Invalid signature attributes'); |
| 65 | + assert(signature instanceof Uint8Array, 'Invalid signature'); |
| 66 | + assert.equal(attributes.size, 1, 'Invalid signature attributes'); |
| 67 | + |
| 68 | + const [keyType, publicKey] = [...attributes][0]; |
| 69 | + assert( |
| 70 | + SIGNATURE_ATTRIBUTE_TO_TYPE_MAPPING.has(keyType), |
| 71 | + 'Invalid signature attribute key type' |
| 72 | + ); |
| 73 | + assert( |
| 74 | + publicKey instanceof Uint8Array, |
| 75 | + 'Invalid signature attribute key' |
| 76 | + ); |
| 77 | + |
| 78 | + integrityBlock.addIntegritySignature({ |
| 79 | + signatureAttributes: { [keyType]: publicKey }, |
| 80 | + signature: Buffer.from(signature), |
| 81 | + }); |
| 82 | + } |
| 83 | + return integrityBlock; |
| 84 | + } catch (err) { |
| 85 | + throw new Error(`Invalid integrity block: ${(err as Error).message}`, { |
| 86 | + cause: err, |
| 87 | + }); |
| 88 | + } |
| 89 | + } |
| 90 | + |
| 91 | + getWebBundleId(): string { |
| 92 | + return this.attributes.get(WEB_BUNDLE_ID_ATTRIBUTE_NAME)!; |
| 93 | + } |
| 94 | + |
| 95 | + setWebBundleId(webBundleId: string) { |
| 96 | + this.attributes.set(WEB_BUNDLE_ID_ATTRIBUTE_NAME, webBundleId); |
| 97 | + } |
| 98 | + |
| 99 | + addIntegritySignature(is: IntegritySignature) { |
| 100 | + this.signatureStack.push(is); |
| 101 | + } |
| 102 | + |
| 103 | + removeIntegritySignature(publicKey: Uint8Array) { |
| 104 | + this.signatureStack = this.signatureStack.filter((integritySignature) => { |
| 105 | + // Uint8Arrays cannot be directly compared, but Buffer can |
| 106 | + return !Buffer.from( |
| 107 | + Object.values(integritySignature.signatureAttributes)[0] |
| 108 | + ).equals(publicKey); |
| 109 | + }); |
| 110 | + } |
| 111 | + |
| 112 | + getSignatureStack(): IntegritySignature[] { |
| 113 | + return this.signatureStack; |
| 114 | + } |
| 115 | + |
| 116 | + toCbor(): Uint8Array { |
| 117 | + return encode([ |
| 118 | + INTEGRITY_BLOCK_MAGIC, |
| 119 | + VERSION_B2, |
| 120 | + this.attributes, |
| 121 | + this.signatureStack.map((integritySig) => { |
| 122 | + // The CBOR must have an array of length 2 containing the following: |
| 123 | + // (0) attributes and (1) signature. The order is important. |
| 124 | + return [integritySig.signatureAttributes, integritySig.signature]; |
| 125 | + }), |
| 126 | + ]); |
| 127 | + } |
| 128 | + |
| 129 | + // Stripped CBOR does not include signatures and is a part of data which hash is signed |
| 130 | + /** @internal */ |
| 131 | + toStrippedCbor(): Uint8Array { |
| 132 | + return encode([INTEGRITY_BLOCK_MAGIC, VERSION_B2, this.attributes, []]); |
| 133 | + } |
| 134 | + |
| 135 | + private static parseSignatureAttributes( |
| 136 | + attributes: SignatureAttributes |
| 137 | + ): [SignatureType, Uint8Array] { |
| 138 | + assert( |
| 139 | + Object.entries(attributes).length == 1, |
| 140 | + 'Invalid signature attributes' |
| 141 | + ); |
| 142 | + const [maybeType, publicKey] = Object.entries(attributes)[0]; |
| 143 | + const type = SIGNATURE_ATTRIBUTE_TO_TYPE_MAPPING.get(maybeType); |
| 144 | + assert(type != undefined, 'Invalid signature attributes'); |
| 145 | + return [type, publicKey]; |
| 146 | + } |
| 147 | +} |
0 commit comments