From 407e96100a963af8bd87896eaafadb3ae15f4327 Mon Sep 17 00:00:00 2001 From: Miles Dai Date: Thu, 11 Jun 2026 14:18:16 -0400 Subject: [PATCH] refactor: encrypt EMWP with native Go XTS instead of cryptsetup Replace the cryptsetup/device-mapper EMWP encryption path with a pure-Go AES-XTS-plain64 implementation (new crypt package) that streams the ciphertext directly into the GPT image. Encrypting in userspace lets the packer drop: - the --privileged container (no loop device or device-mapper at pack time) - the derived volume key written to a temp file in the output dir - the global /dev/mapper name and its cleanup/collision races VerifyEMWP now decrypts in Go and runs veritysetup verify over the plaintext. To maintain parity with the kernel, we keep a golden test vector and add a kernel round-trip test that ensures the encrypted volume can be opened with dmcrypt. --- Dockerfile | 1 + cmd/modelwrap/launcher.go | 4 - cmd/modelwrap/launcher_test.go | 9 +- crypt/crypt.go | 160 +++++++++++++++++++++++++++++ crypt/crypt_test.go | 177 +++++++++++++++++++++++++++++++++ crypt/testdata/ciphertext.bin | Bin 0 -> 24576 bytes crypt/testdata/gen-golden.sh | 46 +++++++++ crypt/testdata/key.bin | Bin 0 -> 64 bytes crypt/testdata/plaintext.bin | 1 + go.mod | 4 +- go.sum | 2 + integration_test.go | 150 ++++++++++++++++++++++++++++ test/e2e.sh | 3 +- wrap/wrap.go | 130 ++++++++++-------------- 14 files changed, 600 insertions(+), 87 deletions(-) create mode 100644 crypt/crypt.go create mode 100644 crypt/crypt_test.go create mode 100644 crypt/testdata/ciphertext.bin create mode 100755 crypt/testdata/gen-golden.sh create mode 100644 crypt/testdata/key.bin create mode 100644 crypt/testdata/plaintext.bin diff --git a/Dockerfile b/Dockerfile index 2d55dc6..25d494f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY *.go ./ +COPY crypt ./crypt COPY wrap ./wrap COPY cmd ./cmd RUN CGO_ENABLED=0 go build -trimpath -buildvcs=false -ldflags=-buildid= -o /modelwrap ./cmd/modelwrap diff --git a/cmd/modelwrap/launcher.go b/cmd/modelwrap/launcher.go index 2e5b346..f80024b 100644 --- a/cmd/modelwrap/launcher.go +++ b/cmd/modelwrap/launcher.go @@ -49,10 +49,6 @@ func launch(opts cliOptions) int { // of the same CLI inside the packer image. func dockerRunArgs(opts cliOptions) ([]string, error) { args := []string{"run", "--rm"} - if opts.Encrypt { - // EMWP packing needs loop device and device-mapper access. - args = append(args, "--privileged") - } hostDir := func(path, fallback string) (string, error) { if path == "" { diff --git a/cmd/modelwrap/launcher_test.go b/cmd/modelwrap/launcher_test.go index e3a6ef1..be97594 100644 --- a/cmd/modelwrap/launcher_test.go +++ b/cmd/modelwrap/launcher_test.go @@ -38,7 +38,7 @@ func TestDockerRunArgs(t *testing.T) { } want := []string{ - "run", "--rm", "--privileged", + "run", "--rm", "-v", filepath.Join(dir, "output") + ":/output", "-v", filepath.Join(dir, "cache") + ":/cache", "-v", filepath.Join(dir, "weights") + ":/model:ro", @@ -54,6 +54,13 @@ func TestDockerRunArgs(t *testing.T) { t.Fatalf("dockerRunArgs mismatch:\n got %q\nwant %q", got, want) } + // EMWP packing is fully userspace and must not request privilege. + for _, arg := range got { + if arg == "--privileged" { + t.Fatal("EMWP packing must not be privileged") + } + } + // Secret values must never appear in the docker command line. for _, arg := range got { if arg == "secret" { diff --git a/crypt/crypt.go b/crypt/crypt.go new file mode 100644 index 0000000..31ba10c --- /dev/null +++ b/crypt/crypt.go @@ -0,0 +1,160 @@ +// Package crypt implements the EMWP dm-crypt encryption (aes-xts-plain64, +// 512-bit key, 4096-byte sectors) in pure Go. +// +// The output is byte-identical to `cryptsetup open --type plain` with the +// parameters the packer uses, so the kernel dm-crypt consumer decrypts +// modelwrap artifacts unchanged. Producing the ciphertext in userspace lets +// the packer encrypt EMWP artifacts without cryptsetup, loop devices, +// device-mapper, or a privileged container, and without writing the volume +// key to disk. +// +// Only the IV-per-sector convention is dm-crypt specific; everything else +// (the two-key split, the tweak derivation, the little-endian sector +// encoding) is standard XTS as implemented by golang.org/x/crypto/xts. That +// one convention is pinned by the dm-crypt golden-vector test. +package crypt + +import ( + "crypto/aes" + "fmt" + "io" + + "golang.org/x/crypto/xts" + + "github.com/tinfoilsh/modelwrap" +) + +// SectorSize is the dm-crypt data-unit size (cryptsetup --sector-size). Each +// SectorSize-byte block is encrypted as one XTS data unit: the tweak is +// derived once from the sector's IV and chained by the GF(2^128) multiply +// across the block's 16-byte AES blocks. +const SectorSize = modelwrap.EMWPSectorSize + +// ivSectorRatio converts a SectorSize-byte sector index into the 512-byte +// sector number that plain64 uses for the IV. dm-crypt keeps 512-byte IV +// numbering unless cryptsetup is given --iv-large-sectors, which the packer +// does not use, so each 4096-byte sector advances the IV by 8. This is the +// single dm-crypt specific convention; if TestDmcryptGolden ever fails, this +// constant (8 vs 1) is the first thing to check. +const ivSectorRatio = SectorSize / 512 + +// streamChunkBytes is the streaming buffer size: 4 MiB, sector-aligned, +// matching the old copyToDevice buffer so large artifacts never load fully +// into memory. +const streamChunkBytes = 1024 * SectorSize + +// The packer and consumer always open dm-crypt with skip 0, so the volume's +// first sector is IV unit 0; there is no non-zero-skip path to support. + +func newCipher(volumeKey []byte) (*xts.Cipher, error) { + if len(volumeKey) != modelwrap.EMWPKeyBytes { + return nil, fmt.Errorf("volume key is %d bytes, want %d", len(volumeKey), modelwrap.EMWPKeyBytes) + } + return xts.NewCipher(aes.NewCipher, volumeKey) +} + +// transform encrypts or decrypts a sector-aligned buffer in place. baseUnit +// is the 0-based sector index of buf[0] within the volume. +func transform(c *xts.Cipher, buf []byte, baseUnit uint64, decrypt bool) { + for off := 0; off < len(buf); off += SectorSize { + iv := (baseUnit + uint64(off/SectorSize)) * ivSectorRatio + s := buf[off : off+SectorSize] + if decrypt { + c.Decrypt(s, s, iv) + } else { + c.Encrypt(s, s, iv) + } + } +} + +// Encrypt encrypts a whole sector-aligned plaintext with the raw 64-byte +// dm-crypt volume key (already derived via modelwrap.DeriveKey). It is a +// convenience for small buffers and tests; the packer uses EncryptStream. +func Encrypt(volumeKey, plaintext []byte) ([]byte, error) { + return inMemory(volumeKey, plaintext, false) +} + +// Decrypt is the inverse of Encrypt. +func Decrypt(volumeKey, ciphertext []byte) ([]byte, error) { + return inMemory(volumeKey, ciphertext, true) +} + +func inMemory(volumeKey, in []byte, decrypt bool) ([]byte, error) { + if len(in)%SectorSize != 0 { + return nil, fmt.Errorf("data length %d is not a multiple of sector size %d", len(in), SectorSize) + } + c, err := newCipher(volumeKey) + if err != nil { + return nil, err + } + out := make([]byte, len(in)) + copy(out, in) + transform(c, out, 0, decrypt) + return out, nil +} + +// EncryptStream reads plaintext from src and writes ciphertext to dst, +// encrypting in sector-aligned chunks so large artifacts never load fully +// into memory. It returns the number of ciphertext bytes written (always a +// multiple of SectorSize). +// +// A trailing partial sector is zero-padded. In practice MWP images are always +// sector-aligned (the EROFS image and the dm-verity hash tree are both whole +// multiples of the 4096-byte block), so real artifacts encrypt with no +// padding and the padding path is purely defensive. +func EncryptStream(volumeKey []byte, dst io.Writer, src io.Reader) (int64, error) { + return stream(volumeKey, dst, src, false) +} + +// DecryptStream is the inverse of EncryptStream. Its input must be +// sector-aligned (ciphertext always is); a trailing partial sector is an +// error rather than being padded. +func DecryptStream(volumeKey []byte, dst io.Writer, src io.Reader) (int64, error) { + return stream(volumeKey, dst, src, true) +} + +func stream(volumeKey []byte, dst io.Writer, src io.Reader, decrypt bool) (int64, error) { + c, err := newCipher(volumeKey) + if err != nil { + return 0, err + } + buf := make([]byte, streamChunkBytes) + var baseUnit uint64 + var written int64 + for { + n, readErr := io.ReadFull(src, buf) + + // A short read from io.ReadFull means either end of stream (io.EOF or + // io.ErrUnexpectedEOF) or a genuine read error. Only end of stream + // justifies padding a trailing partial sector; on a real error we must + // not emit that (incorrectly padded) ciphertext, so surface the error + // before writing anything. + eof := readErr == io.EOF || readErr == io.ErrUnexpectedEOF + if readErr != nil && !eof { + return written, readErr + } + + if n > 0 { + full := n + if rem := n % SectorSize; rem != 0 { + if decrypt { + return written, fmt.Errorf("ciphertext is not sector-aligned (trailing %d bytes)", rem) + } + // A partial sector here implies clean EOF, so the tail is the + // real end of the data: zero-pad it to a full sector. + full = n - rem + SectorSize + clear(buf[n:full]) + } + transform(c, buf[:full], baseUnit, decrypt) + if _, err := dst.Write(buf[:full]); err != nil { + return written, err + } + baseUnit += uint64(full / SectorSize) + written += int64(full) + } + + if eof { + return written, nil + } + } +} diff --git a/crypt/crypt_test.go b/crypt/crypt_test.go new file mode 100644 index 0000000..b700a3c --- /dev/null +++ b/crypt/crypt_test.go @@ -0,0 +1,177 @@ +package crypt + +import ( + "bytes" + "errors" + "os" + "path/filepath" + "testing" +) + +// TestDmcryptGolden is the authoritative compatibility check: it asserts +// crypt.Encrypt reproduces, byte for byte, the ciphertext that the real +// cryptsetup produced for the same key and plaintext with the packer's exact +// flags (see testdata/gen-golden.sh). Passing this proves the kernel +// dm-crypt consumer will decrypt artifacts this package encrypts, and pins +// the one dm-crypt specific convention (the per-sector IV) without needing +// cryptsetup at test time. +func TestDmcryptGolden(t *testing.T) { + key := readTestdata(t, "key.bin") + pt := readTestdata(t, "plaintext.bin") + ct := readTestdata(t, "ciphertext.bin") + + got, err := Encrypt(key, pt) + if err != nil { + t.Fatalf("Encrypt: %v", err) + } + if !bytes.Equal(got, ct) { + t.Fatalf("ciphertext does not match dm-crypt golden vector;\n"+ + "the per-sector IV convention is likely wrong (try ivSectorRatio=1).\n"+ + "first mismatch at byte %d", firstDiff(got, ct)) + } + + back, err := Decrypt(key, ct) + if err != nil { + t.Fatalf("Decrypt: %v", err) + } + if !bytes.Equal(back, pt) { + t.Fatal("Decrypt(golden ciphertext) != plaintext") + } +} + +// TestDmcryptGoldenStream checks the streaming path produces the same golden +// ciphertext and round-trips, including across chunk boundaries. +func TestDmcryptGoldenStream(t *testing.T) { + key := readTestdata(t, "key.bin") + pt := readTestdata(t, "plaintext.bin") + ct := readTestdata(t, "ciphertext.bin") + + var enc bytes.Buffer + if _, err := EncryptStream(key, &enc, bytes.NewReader(pt)); err != nil { + t.Fatalf("EncryptStream: %v", err) + } + if !bytes.Equal(enc.Bytes(), ct) { + t.Fatalf("streamed ciphertext != golden (first mismatch at %d)", firstDiff(enc.Bytes(), ct)) + } + + var dec bytes.Buffer + if _, err := DecryptStream(key, &dec, bytes.NewReader(ct)); err != nil { + t.Fatalf("DecryptStream: %v", err) + } + if !bytes.Equal(dec.Bytes(), pt) { + t.Fatal("DecryptStream(golden) != plaintext") + } +} + +// TestStreamPadsTrailingSector confirms a non-sector-aligned plaintext is +// zero-padded on encrypt and recovered (with padding) on decrypt, matching +// the old backing-file behavior. +func TestStreamPadsTrailingSector(t *testing.T) { + key := bytes.Repeat([]byte{0x5A}, 64) + pt := bytes.Repeat([]byte{0xEE}, 2*SectorSize+123) // unaligned + + var enc bytes.Buffer + n, err := EncryptStream(key, &enc, bytes.NewReader(pt)) + if err != nil { + t.Fatalf("EncryptStream: %v", err) + } + if n != 3*SectorSize || enc.Len() != 3*SectorSize { + t.Fatalf("padded ciphertext = %d bytes, want %d", enc.Len(), 3*SectorSize) + } + + var dec bytes.Buffer + if _, err := DecryptStream(key, &dec, &enc); err != nil { + t.Fatalf("DecryptStream: %v", err) + } + if !bytes.Equal(dec.Bytes()[:len(pt)], pt) { + t.Fatal("decrypted prefix != original plaintext") + } + for i := len(pt); i < dec.Len(); i++ { + if dec.Bytes()[i] != 0 { + t.Fatalf("pad byte at %d = %d, want 0", i, dec.Bytes()[i]) + } + } +} + +// TestIdenticalSectorsDistinctCiphertext confirms the per-sector IV is +// actually applied: identical plaintext sectors at different offsets must +// encrypt to different ciphertext. testdata's sector 5 duplicates sector 0. +func TestIdenticalSectorsDistinctCiphertext(t *testing.T) { + key := readTestdata(t, "key.bin") + pt := readTestdata(t, "plaintext.bin") + if !bytes.Equal(pt[0:SectorSize], pt[5*SectorSize:6*SectorSize]) { + t.Skip("testdata sector 5 no longer duplicates sector 0") + } + ct, err := Encrypt(key, pt) + if err != nil { + t.Fatalf("Encrypt: %v", err) + } + if bytes.Equal(ct[0:SectorSize], ct[5*SectorSize:6*SectorSize]) { + t.Fatal("identical plaintext sectors produced identical ciphertext; IV not per-sector") + } +} + +func TestRejectsBadInput(t *testing.T) { + good := bytes.Repeat([]byte{1}, 64) + if _, err := Encrypt(good[:32], make([]byte, SectorSize)); err == nil { + t.Fatal("expected error for short key") + } + if _, err := Encrypt(good, make([]byte, SectorSize+1)); err == nil { + t.Fatal("expected error for non-sector-multiple length") + } + if _, err := DecryptStream(good, &bytes.Buffer{}, bytes.NewReader(make([]byte, SectorSize+1))); err == nil { + t.Fatal("expected error decrypting non-sector-aligned ciphertext") + } +} + +// errReader yields data once together with a non-EOF error, mimicking a +// reader that fails mid-stream after a partial, non-sector-aligned read. +type errReader struct { + data []byte + err error + done bool +} + +func (r *errReader) Read(p []byte) (int, error) { + if r.done { + return 0, r.err + } + r.done = true + return copy(p, r.data), r.err +} + +// TestEncryptStreamReadErrorNoWrite guards against treating a failed partial +// read as end-of-stream: a genuine read error must surface without emitting +// any (zero-padded) ciphertext. +func TestEncryptStreamReadErrorNoWrite(t *testing.T) { + key := bytes.Repeat([]byte{0x11}, 64) + boom := errors.New("boom") + r := &errReader{data: bytes.Repeat([]byte{0xCD}, 100), err: boom} // 100 % SectorSize != 0 + + var dst bytes.Buffer + n, err := EncryptStream(key, &dst, r) + if !errors.Is(err, boom) { + t.Fatalf("err = %v, want boom", err) + } + if n != 0 || dst.Len() != 0 { + t.Fatalf("wrote %d bytes (reported %d) despite read error; want none", dst.Len(), n) + } +} + +func readTestdata(t *testing.T, name string) []byte { + t.Helper() + b, err := os.ReadFile(filepath.Join("testdata", name)) + if err != nil { + t.Fatalf("reading testdata/%s: %v", name, err) + } + return b +} + +func firstDiff(a, b []byte) int { + for i := 0; i < len(a) && i < len(b); i++ { + if a[i] != b[i] { + return i + } + } + return min(len(a), len(b)) +} diff --git a/crypt/testdata/ciphertext.bin b/crypt/testdata/ciphertext.bin new file mode 100644 index 0000000000000000000000000000000000000000..eea22225eb1beb25833fc5bd910f3b041fafb5a9 GIT binary patch literal 24576 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 literal 0 HcmV?d00001 diff --git a/crypt/testdata/gen-golden.sh b/crypt/testdata/gen-golden.sh new file mode 100755 index 0000000..a06b787 --- /dev/null +++ b/crypt/testdata/gen-golden.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Regenerate the dm-crypt golden vector used by TestDmcryptGolden: +# +# key.bin 64-byte volume key (bytes 0x00..0x3f). +# plaintext.bin Six 4096-byte sectors with distinct fill bytes; sector 5 +# repeats sector 0 so the vector exercises identical +# plaintext at distinct IVs. +# ciphertext.bin plaintext.bin encrypted by the real cryptsetup with the +# exact flags the packer uses (aes-xts-plain64, 512-bit key, +# 4096-byte sectors, skip 0). +# +# crypt.Encrypt must reproduce ciphertext.bin byte for byte, which pins the +# packer's native-Go XTS to kernel dm-crypt. Everything runs inside one +# privileged container, so the only host requirement is Docker: +# +# ./crypt/testdata/gen-golden.sh +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" + +docker run --rm --privileged -i -v "$PWD:/td" debian:trixie-slim bash -s <<'SCRIPT' +set -euo pipefail +S=4096 + +# emit_sector N writes S copies of the byte value N to stdout. +emit_sector() { printf "\\x$(printf '%02x' "$1")%.0s" $(seq "$S"); } + +# key.bin: the 64 bytes 0x00..0x3f. +for i in $(seq 0 63); do printf "\\x$(printf '%02x' "$i")"; done > /td/key.bin + +# plaintext.bin: sectors filled with 0xA0..0xA4, then a sixth repeating 0xA0. +{ for v in 160 161 162 163 164; do emit_sector "$v"; done; emit_sector 160; } > /td/plaintext.bin + +# ciphertext.bin: encrypt with the real cryptsetup, matching the packer flags. +# The cryptsetup version is not pinned: dm-crypt aes-xts-plain64 output is +# kernel/format-determined and stable across cryptsetup versions. +apt-get update -qq && apt-get install -y -qq cryptsetup-bin >/dev/null +cp /td/plaintext.bin /work_ct +cryptsetup open --type plain --cipher aes-xts-plain64 --key-size 512 \ + --sector-size 4096 --key-file /td/key.bin --skip 0 /work_ct golden +dd if=/td/plaintext.bin of=/dev/mapper/golden bs=4096 conv=notrunc status=none +sync +cryptsetup close golden +cp /work_ct /td/ciphertext.bin +SCRIPT + +echo "wrote key.bin ($(wc -c < key.bin)B), plaintext.bin ($(wc -c < plaintext.bin)B), ciphertext.bin ($(wc -c < ciphertext.bin)B)" diff --git a/crypt/testdata/key.bin b/crypt/testdata/key.bin new file mode 100644 index 0000000000000000000000000000000000000000..96eb299ab61d459148b19b03f71386abcec74669 GIT binary patch literal 64 zcmZQzWMXDvWn<^yMC+6cQE@6%&_`l#-T_m6KOcR8m$^Ra4i{)Y8_`)zddH TG%_|ZH8Z!cw6eCbwX+8Rs^ACV literal 0 HcmV?d00001 diff --git a/crypt/testdata/plaintext.bin b/crypt/testdata/plaintext.bin new file mode 100644 index 0000000..1737b58 --- /dev/null +++ b/crypt/testdata/plaintext.bin @@ -0,0 +1 @@ +                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                ĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦĦ˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘˘££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££££¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 \ No newline at end of file diff --git a/go.mod b/go.mod index 0cefaa8..8af1e2c 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,9 @@ module github.com/tinfoilsh/modelwrap -go 1.25 +go 1.25.0 require github.com/google/uuid v1.6.0 require golang.org/x/mod v0.31.0 + +require golang.org/x/crypto v0.52.0 diff --git a/go.sum b/go.sum index 5d7866a..258723a 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= diff --git a/integration_test.go b/integration_test.go index 4f69f7b..6e4b7c5 100644 --- a/integration_test.go +++ b/integration_test.go @@ -5,6 +5,7 @@ package modelwrap_test import ( "encoding/base64" "fmt" + "io" "os" "os/exec" "path/filepath" @@ -114,3 +115,152 @@ func TestEMWPRoundTripIntegration(t *testing.T) { } } } + +// TestEMWPGoEncryptKernelDecrypt is the authoritative compatibility test for +// the native-Go dm-crypt encryption: it packs a local directory as EMWP +// (encrypted entirely in userspace by package crypt, no cryptsetup) and then +// decrypts it with the real kernel dm-crypt via cryptsetup, verifies it with +// kernel dm-verity, and mounts it. If the Go ciphertext were not byte-exact +// dm-crypt output, the kernel would decrypt garbage and dm-verity would fail. +// It needs no network or huggingface tooling, only a privileged Linux +// environment with erofs-utils, cryptsetup, and gdisk. +func TestEMWPGoEncryptKernelDecrypt(t *testing.T) { + if os.Getenv("TINFOIL_MODELWRAP_INTEGRATION") != "1" { + t.Skip("set TINFOIL_MODELWRAP_INTEGRATION=1 to run") + } + + work := t.TempDir() + modelDir := filepath.Join(work, "model") + if err := os.MkdirAll(modelDir, 0755); err != nil { + t.Fatal(err) + } + // Content large enough to span many 4096-byte sectors, so the kernel + // must walk the per-sector IV across the whole partition to recover it. + modelFiles := map[string][]byte{ + "config.json": []byte(`{"model_type":"test","n":12345}`), + "pytorch_model.bin": func() []byte { + b := make([]byte, 512*1024) + for i := range b { + b[i] = byte(i*131 + 7) + } + return b + }(), + } + for name, content := range modelFiles { + if err := os.WriteFile(filepath.Join(modelDir, name), content, 0644); err != nil { + t.Fatal(err) + } + } + + masterKey := []byte(strings.Repeat("k", modelwrap.EMWPMasterKeyBytes)) + keyFile := filepath.Join(work, "master.key") + if err := os.WriteFile(keyFile, []byte(base64.StdEncoding.EncodeToString(masterKey)), 0600); err != nil { + t.Fatal(err) + } + + // Pack with the Go encryptor. Verify:false on purpose: the only + // verification that matters here is the kernel's, performed below. + rawRef, err := wrap.Pack(wrap.Options{ + Model: "testmodel@v1", + ModelDir: modelDir, + CacheDir: filepath.Join(work, "cache"), + OutputDir: filepath.Join(work, "output"), + Encrypt: true, + Verify: false, + KeyFile: keyFile, + }) + if err != nil { + t.Fatalf("packing EMWP: %v", err) + } + ref, err := modelwrap.ParseRef(rawRef) + if err != nil { + t.Fatalf("parsing packed ref %q: %v", rawRef, err) + } + + emwpFile := filepath.Join(work, "output", "testmodel", "v1.emwp") + fi, err := os.Stat(emwpFile) + if err != nil { + t.Fatal(err) + } + + // Expose the encrypted payload partition exactly as the consumer does. + partOffset := int64(modelwrap.EMWPPartitionStartSector * modelwrap.GPTSectorSize) + partSize := fi.Size() - partOffset - int64(modelwrap.EMWPGPTTrailingSectors*modelwrap.GPTSectorSize) + out, err := exec.Command( + "losetup", "--read-only", "--find", "--show", + "--offset", fmt.Sprint(partOffset), + "--sizelimit", fmt.Sprint(partSize), + emwpFile, + ).Output() + if err != nil { + t.Fatalf("losetup: %v", err) + } + loopDev := strings.TrimSpace(string(out)) + t.Cleanup(func() { _ = exec.Command("losetup", "-d", loopDev).Run() }) + + dmKey, err := modelwrap.DeriveKey(masterKey, ref) + if err != nil { + t.Fatalf("deriving dm-crypt key: %v", err) + } + dmKeyFile := filepath.Join(work, "dm.key") + if err := os.WriteFile(dmKeyFile, dmKey, 0600); err != nil { + t.Fatal(err) + } + + // Kernel dm-crypt decrypt of the Go-produced ciphertext. If the Go XTS + // output were not byte-exact dm-crypt ciphertext, the kernel would + // produce different plaintext through this mapping. + cryptName := "modelwrap-goit-crypt" + if err := unwrap.OpenCrypt(loopDev, cryptName, dmKeyFile); err != nil { + t.Fatalf("kernel cryptsetup open of Go ciphertext: %v", err) + } + t.Cleanup(func() { unwrap.CloseCrypt(cryptName) }) + + // Authoritative check: the bytes the kernel dm-crypt produces must equal + // the original MWP plaintext the packer encrypted in Go. + plain, err := os.ReadFile(filepath.Join(work, "output", "testmodel", "v1.mpk")) + if err != nil { + t.Fatal(err) + } + dev, err := os.Open("/dev/mapper/" + cryptName) + if err != nil { + t.Fatal(err) + } + defer dev.Close() + got := make([]byte, len(plain)) + if _, err := io.ReadFull(dev, got); err != nil { + t.Fatalf("reading kernel-decrypted device: %v", err) + } + if !bytesEqual(got, plain) { + t.Fatalf("kernel dm-crypt decryption does not match the original MWP plaintext "+ + "(first diff at byte %d): Go ciphertext is not dm-crypt compatible", firstDiffIdx(got, plain)) + } + + // Independent integrity check, in userspace so it needs no dm-verity + // kernel target: the kernel-decrypted plaintext must satisfy the attested + // root hash. + if err := wrap.VerifyMWP("/dev/mapper/"+cryptName, filepath.Join(work, "output", "testmodel", "v1.info")); err != nil { + t.Fatalf("veritysetup verify over kernel-decrypted plaintext: %v", err) + } +} + +func firstDiffIdx(a, b []byte) int { + for i := 0; i < len(a) && i < len(b); i++ { + if a[i] != b[i] { + return i + } + } + return min(len(a), len(b)) +} + +func bytesEqual(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/test/e2e.sh b/test/e2e.sh index cacabdb..7535f00 100755 --- a/test/e2e.sh +++ b/test/e2e.sh @@ -44,10 +44,11 @@ docker run --rm --privileged \ -test.run TestEMWPRoundTripIntegration -test.v # CLI smoke test: the user-facing entrypoint with volumes and key file. +# Packing is userspace-only, so it runs unprivileged. mkdir -p "${WORK_DIR}/cache" "${WORK_DIR}/output" printf '%s\n' "${KEY_B64}" > "${WORK_DIR}/emwp-key" -docker run --rm --privileged \ +docker run --rm \ -v "${WORK_DIR}/cache:/cache" \ -v "${WORK_DIR}/emwp-key:/run/emwp-key:ro" \ -v "${WORK_DIR}/output:/output" \ diff --git a/wrap/wrap.go b/wrap/wrap.go index b1db99d..b429c85 100644 --- a/wrap/wrap.go +++ b/wrap/wrap.go @@ -17,10 +17,10 @@ import ( "os" "os/exec" "path/filepath" - "strconv" "strings" "github.com/tinfoilsh/modelwrap" + "github.com/tinfoilsh/modelwrap/crypt" ) // Options configures a single packing run. Model is a Hugging Face model @@ -318,24 +318,17 @@ func encryptEMWP(mwpFile, emwpFile string, ref *modelwrap.ArtifactRef, masterKey totalSectors := endSector + 1 + modelwrap.EMWPGPTTrailingSectors diskUUID := modelwrap.UUIDv5URL(ref.RootHash + "-emwp-disk") tmpFile := emwpFile + ".tmp" - dmKeyFile := emwpFile + ".key.tmp" - mapperName := "modelwrap-emwp-" + ref.RootHash[:16] dmKey, err := modelwrap.DeriveKey(masterKey, ref) if err != nil { return err } + defer clear(dmKey) - for _, path := range []string{tmpFile, dmKeyFile} { - if err := os.Remove(path); err != nil && !os.IsNotExist(err) { - return err - } + if err := os.Remove(tmpFile); err != nil && !os.IsNotExist(err) { + return err } - defer func() { - closeCryptMapper(mapperName) - os.Remove(dmKeyFile) - os.Remove(tmpFile) - }() + defer os.Remove(tmpFile) fmt.Printf("Creating EMWP GPT image %s\n", emwpFile) if err := createSparseFile(tmpFile, totalSectors*modelwrap.GPTSectorSize); err != nil { @@ -355,33 +348,31 @@ func encryptEMWP(mwpFile, emwpFile string, ref *modelwrap.ArtifactRef, masterKey return err } - if err := os.WriteFile(dmKeyFile, dmKey, 0600); err != nil { + if err := encryptPayload(tmpFile, mwpFile, dmKey); err != nil { return err } - err = run(exec.Command( - "cryptsetup", "open", - "--type", "plain", - "--cipher", modelwrap.EMWPCipher, - "--key-size", strconv.Itoa(modelwrap.EMWPKeySizeBits), - "--sector-size", strconv.Itoa(modelwrap.EMWPSectorSize), - "--key-file", dmKeyFile, - "--offset", strconv.Itoa(modelwrap.EMWPPartitionStartSector), - "--skip", "0", - "--size", strconv.FormatInt(sectors, 10), - tmpFile, - mapperName, - )) + return os.Rename(tmpFile, emwpFile) +} + +// encryptPayload streams the dm-crypt encryption of mwpFile into the +// encrypted payload partition of the GPT image at imgFile. +func encryptPayload(imgFile, mwpFile string, dmKey []byte) error { + in, err := os.Open(mwpFile) if err != nil { return err } - - if err := copyToDevice(mwpFile, "/dev/mapper/"+mapperName); err != nil { + defer in.Close() + out, err := os.OpenFile(imgFile, os.O_RDWR, 0) + if err != nil { return err } - if err := closeCryptMapper(mapperName); err != nil { - return err + defer out.Close() + + payload := io.NewOffsetWriter(out, modelwrap.EMWPPartitionStartSector*modelwrap.GPTSectorSize) + if _, err := crypt.EncryptStream(dmKey, payload, in); err != nil { + return fmt.Errorf("encrypting %s: %w", mwpFile, err) } - return os.Rename(tmpFile, emwpFile) + return out.Sync() } // VerifyMWP runs an offline dm-verity verification of an MWP artifact @@ -415,8 +406,8 @@ func VerifyMWP(mwpFile, infoFile string) error { return nil } -// VerifyEMWP decrypts an EMWP artifact through a temporary dm-crypt -// mapping and verifies the inner dm-verity tree. +// VerifyEMWP decrypts an EMWP artifact's payload partition in userspace and +// verifies the inner dm-verity tree of the recovered MWP plaintext. func VerifyEMWP(emwpFile, infoFile string, masterKey []byte) error { ref, err := parseInfoFile(infoFile) if err != nil { @@ -427,38 +418,42 @@ func VerifyEMWP(emwpFile, infoFile string, masterKey []byte) error { return fmt.Errorf("EMWP artifact not found: %s", emwpFile) } - sectors := fi.Size()/modelwrap.GPTSectorSize - modelwrap.EMWPPartitionStartSector - modelwrap.EMWPGPTTrailingSectors - dmKeyFile := emwpFile + ".key.tmp" - mapperName := "modelwrap-emwp-verify-" + ref.RootHash[:16] dmKey, err := modelwrap.DeriveKey(masterKey, ref) if err != nil { return err } + defer clear(dmKey) - defer func() { - closeCryptMapper(mapperName) - os.Remove(dmKeyFile) - }() - if err := os.WriteFile(dmKeyFile, dmKey, 0600); err != nil { + plainFile := emwpFile + ".verify.tmp" + defer os.Remove(plainFile) + if err := decryptPayload(emwpFile, plainFile, fi.Size(), dmKey); err != nil { return err } - err = run(exec.Command( - "cryptsetup", "open", - "--type", "plain", - "--cipher", modelwrap.EMWPCipher, - "--key-size", strconv.Itoa(modelwrap.EMWPKeySizeBits), - "--sector-size", strconv.Itoa(modelwrap.EMWPSectorSize), - "--key-file", dmKeyFile, - "--offset", strconv.Itoa(modelwrap.EMWPPartitionStartSector), - "--skip", "0", - "--size", strconv.FormatInt(sectors, 10), - emwpFile, - mapperName, - )) + return VerifyMWP(plainFile, infoFile) +} + +// decryptPayload streams the decrypted MWP plaintext of an EMWP image's +// payload partition into plainFile. +func decryptPayload(emwpFile, plainFile string, emwpSize int64, dmKey []byte) error { + partOffset := int64(modelwrap.EMWPPartitionStartSector * modelwrap.GPTSectorSize) + partSize := emwpSize - partOffset - int64(modelwrap.EMWPGPTTrailingSectors*modelwrap.GPTSectorSize) + + in, err := os.Open(emwpFile) if err != nil { return err } - return VerifyMWP("/dev/mapper/"+mapperName, infoFile) + defer in.Close() + out, err := os.OpenFile(plainFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer out.Close() + + payload := io.NewSectionReader(in, partOffset, partSize) + if _, err := crypt.DecryptStream(dmKey, out, payload); err != nil { + return fmt.Errorf("decrypting EMWP payload: %w", err) + } + return out.Sync() } // LoadMasterKey loads the EMWP master key from keyFile if set, else from @@ -504,31 +499,6 @@ func createSparseFile(path string, size int64) error { return f.Truncate(size) } -// copyToDevice writes src into the block device dst and syncs it. -func copyToDevice(src, dst string) error { - in, err := os.Open(src) - if err != nil { - return err - } - defer in.Close() - out, err := os.OpenFile(dst, os.O_WRONLY, 0) - if err != nil { - return err - } - defer out.Close() - if _, err := io.CopyBuffer(out, in, make([]byte, 4*1024*1024)); err != nil { - return fmt.Errorf("writing %s to %s: %w", src, dst, err) - } - return out.Sync() -} - -func closeCryptMapper(name string) error { - if _, err := os.Stat("/dev/mapper/" + name); os.IsNotExist(err) { - return nil - } - return run(exec.Command("cryptsetup", "close", name)) -} - func run(cmd *exec.Cmd) error { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr