No description
Find a file
Mats Stottmeister 7f58db9cca
feat: harden v1 watermarking pipeline
Reed-Solomon RS(56,40) errata coding (2e+s<=16); Watson 1993 perceptual
masking; AAN scaled DCT; gonum FFT crop+scale sync recovery; adaptive
64/128 px tiles with 4096 px / 16 MP decompression-bomb cap; bounded
per-tile concurrency with ctx-aware row-level cancellation across
toRGBA, splitYCbCr, mergeYCbCr, sync, resample, quality and tile work.
KeyIDSize widened 4 to 8 bytes (PayloadVersion 2) to eliminate 32-bit
hash collisions; HMAC pair label rebumped to verum-pairs-v2. Embed
retry loop fails fast on permanent errors and only retries
ErrQualityGateFailed; pre-encode and post-encode quality gates both
enforced; JPEG output rejects alpha-bearing inputs; paletted PNGs with
tRNS and NRGBA pixels with alpha below the visibility floor preserve
their hidden RGB end-to-end. QualityConfig validates NaN/Inf/range and
non-negative MaxRetries; DetectionConfig.Scales validated, additive,
and capped at 8 entries. Calibration corpus of 50 procedural PNGs
(gradient/dark/portrait/screenshot/noisy x10) gates production
constants within +-30% of corpus-derived percentiles, enforces 80%
per-claim downscale success and 30% aggregate embed-failure ceiling.
Capability claims, known limits, and public API surface are aligned
across README, NOTICE, CHANGELOG, and CONTRIBUTING.
2026-04-29 23:25:43 +02:00
internal feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
testdata feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
.gitignore feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
bench_test.go feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
CHANGELOG.md feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
config.go feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
CONTRIBUTING.md feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
detect.go feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
embed.go feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
errors.go feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
examples_test.go feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
go.mod feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
go.sum feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
LICENSE first commit 2026-04-28 13:02:52 +02:00
NOTICE feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
payload.go feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
payload_test.go feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
README.md feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
sync.go feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
verum.go feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
verum_test.go feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
watson.go feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00
watson_test.go feat: harden v1 watermarking pipeline 2026-04-29 23:25:43 +02:00

mtn-verum

Keyed image marking with measurable distortion bounds.

mtn-verum writes a cryptographically-keyed mark directly into image pixels so images generated by an MTN service can be identified later, without relying on metadata that most uploaders strip.

go get github.com/MTN-Media-Group/mtn-verum

Quick start

import (
    "context"
    "os"
    "time"

    verum "github.com/MTN-Media-Group/mtn-verum"
)

key := verum.Key{ID: "k1", Secret: []byte("a high-entropy secret of at least 16 bytes")}
cfg := verum.Config{
    ActiveKey:     key,
    DetectionKeys: []verum.Key{key},
    Strength:      verum.StrengthBalanced,
    // JPEGQuality: 0 uses the public API default of 95 for JPEG output.
}

payload := verum.Payload{
    GeneratedAt:  time.Now(),
    Provider:     "mtnai",
    Model:        "flux.1-pro",
    GenerationID: "gen_abc123",
}

src, err := os.ReadFile("input.png")
if err != nil {
    panic(err)
}
ok, err := verum.IsEmbeddable(src, cfg)
if err != nil || !ok {
    panic(err)
}
res, err := verum.Embed(context.Background(), src, "image/png", payload, cfg)
if err != nil {
    panic(err)
}
// res.Data carries the marked image; res.Quality has luminance-plane SSIM, PSNR, MaxDelta.

det, err := verum.Detect(context.Background(), res.Data, "image/png", cfg)
if err != nil {
    panic(err)
}
// det.Detected, det.KeyID, det.PayloadDigest, det.Confidence, det.BestScale.

Verify is Detect plus a digest check against an expected Payload.

Key secrets must be at least 16 bytes; 32 bytes or more is recommended.

Current PayloadVersion is 2; the on-image frame carries an 8-byte SHA-256-truncated key ID, a 32-byte HMAC digest, and Reed-Solomon parity in a 60-byte frame.

Capabilities

Capability Current
PNG embed/detect at native scale
JPEG transcode survival at Q95 with StrengthRobust
Small top-left crop offsets up to 2 tile periods, when the original image dimensions are a multiple of the tile size and the cropped image retains at least 5 tiles per side
0.75x bilinear, 0.5x nearest-neighbor, and 2048→1024 bilinear large-image downscale detection with StrengthRobust.
Reed-Solomon: 16 parity bytes; corrects up to 8 byte errors (or 2e + s <= 16 errors+erasures) via syndrome decoding
WebP decode and lossless encode (no CGO)
Multi-key detection / key rotation
Premultiplied alpha round-trip preserved
Fully transparent pixels untouched

Calibration thresholds: the generated corpus fixtures are 512px PNGs; recordScaleSweep pre-resizes each fixture to a 1024px PNG before embedding. The resulting 1024-source downscale points (0.5x nearest 1024→512 and 0.75x bilinear 1024→768) are validated against the calibration corpus at >= 80% success per claim. Full-corpus calibration also enforces <= 30% aggregate embed failures across all profiles. The 2048→1024 large-image path, the 384→288 0.75x boundary, JPEG Q95 round-trip, and crop survival at the retained-tile floor are verified by direct fixture tests, not by corpus sweep.

Downscale survival requires the resized image to keep both dimensions at least MinImageDim (256px), so the shortest side is the limiting dimension. Current measured coverage is 1024→512 for 0.5x nearest-neighbor, 1024→768 for 0.75x bilinear, 2048→1024 for 0.5x bilinear on the large-image path, and 384→288 for 0.75x bilinear at the boundary. The 1024→512 bilinear path is not claimed for PayloadVersion 2.

Known limitations

  • Pure-flat images with no texture and no edges cannot carry the mark; Embed returns ErrNoCapacity, and IsEmbeddable can report this before embedding.
  • Photos with very smooth regions, including extreme close-ups and gradient skies, may have reduced robustness.
  • 1024→512 0.5x bilinear recovery is below the current corpus claim threshold.
  • JPEG below quality 95 is not supported in the current release.
  • JPEG Q75/Q85 transcode survival is not claimed under the default Robust quality gates.
  • Lossy WebP output is not supported; the project is pure-Go and has no CGO bindings to libwebp.

Detection details

DetectResult.Details uses stable numeric keys for observability:

  • bit_confidence_bucket_0 through bit_confidence_bucket_4: fraction of recovered bits in each confidence bucket.
  • supporting_tiles_ratio: supporting tiles divided by checked tiles.
  • tiles_checked: number of candidate tiles inspected.
  • sync_peak_strength: FFT sync peak strength relative to nearby candidate frequencies.
  • scale_estimate: estimated resize factor before detection.
  • crop_x_pixels and crop_y_pixels: estimated grid offset in pixels.
  • Config.Detection.Scales adds caller-supplied scale hints to the default 0.75 and 0.5 sweep; duplicate scales are ignored.

Strength profiles

Profile Default min SSIM Default min PSNR Default max delta Default MaxChangeRatio
Invisible 0.999 50 dB 12 0.2
Balanced 0.997 46 dB 18 0.4
Robust 0.985 38 dB 80 0.6

These thresholds are luminance-plane SSIM/PSNR/MaxDelta checked pre-encode, and re-validated post-encode for lossy formats (JPEG); JPEG output may alter chroma post-encode.

Embed retries with a lower same-profile coefficient delta if the requested gates fail.

Status

Pre-release. The public API and the on-image wire format may still change.

License

GNU AGPL-3.0-only. See LICENSE and NOTICE. MTN Media Group, as copyright holder, reserves the right to use, modify, and distribute this software under alternative license terms, including in proprietary projects.

Contributing

See CONTRIBUTING.md. Contributions are dual-licensed: public AGPL-3.0-only plus a perpetual, irrevocable grant to MTN Media Group permitting proprietary use.

Prior art

This is an independent implementation of frequency-domain keyed image watermarking, a technique with decades of public prior art. The canonical citation is in NOTICE. It is not derived from, affiliated with, or compatible with any third-party watermarking system.