| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
Ecluse.Package.Integrity
Description
Integrity-algorithm strength and the public-admission integrity floor.
Écluse trusts a digest only as far as its algorithm is collision-resistant, and it trusts the two upstreams asymmetrically: a trusted private upstream's versions are vetted out of band, so a weak (legacy SHA-1) digest there is acceptable, but an untrusted public upstream's bytes are tamper-evident only through a strong digest. This module is the one place that ranks algorithms by strength and decides what clears the floor, so the worker's tamper gate and the serve layer's admission gate share a single notion of "strong enough" rather than each re-encoding the ranking.
The strength ranking
integrityStrength orders algorithms by collision resistance: the broken ones
(MD5, SHA-1) rank below the SHA-256 floor; SHA-256 and the modern long digests rank
at or above it. assertedAlg resolves what a Hash claims — its tag directly, or
for a Subresource-Integrity string the algorithm named in its <alg>-<base64>
prefix — so an SRI is ranked and floored by the algorithm it embeds.
The public-integrity floor
A MinIntegrity is the configured minimum algorithm a public version's digest
must meet to be admitted. It is opaque and hard-floored at SHA-256: it can be
raised (to SHA-512 or Blake2b, as cryptanalysis ages an algorithm) but never set
below SHA-256, because admitting a public version on a SHA-1 digest would let a
collision substitute its bytes. The trusted private path never consults the floor —
trust substitutes for crypto strength there (see
docs/architecture/security.md → "Asymmetric integrity trust").
Synopsis
- data Strength
- integrityStrength :: HashAlg -> Strength
- assertedAlg :: Hash -> Maybe HashAlg
- renderHashAlg :: HashAlg -> Text
- parseHashAlg :: Text -> Either Text HashAlg
- sriAlgorithm :: Text -> Maybe HashAlg
- sriPrefix :: Text -> Text
- sriBody :: Text -> Text
- data MinIntegrity
- defaultMinIntegrity :: MinIntegrity
- mkMinIntegrity :: HashAlg -> Either Text MinIntegrity
- parseMinIntegrity :: Text -> Either Text MinIntegrity
- unMinIntegrity :: MinIntegrity -> HashAlg
- renderMinIntegrity :: MinIntegrity -> Text
- meetsFloor :: MinIntegrity -> HashAlg -> Bool
- data VersionIntegrity
- classifyArtifacts :: MinIntegrity -> NonEmpty Artifact -> VersionIntegrity
Algorithm strength
The collision-resistance tier of a hash algorithm, with constructors ordered
weakest to strongest so the derived Ord is the strength ranking: two tiers
compare by collision resistance, and equal-strength algorithms share a tier (so they
compare EQ). This is the one named ranking the worker's tamper gate and the serve
layer's admission floor both consult.
integrityStrength :: HashAlg -> Strength Source #
The collision-resistance Strength tier of an algorithm; __a stronger algorithm
ranks higher__ under Strength's Ord.
The broken algorithms rank below the SHA-256 floor ():
MD5 and SHA-1 have practical collisions, so a match on one cannot prove the bytes
were not substituted. SHA-256 and the longer digests rank at or above the floor:
SHA-384 above it in a tier of its own, then SHA-512 and Blake2b sharing the top tier
(equal strength). A bare integrityStrength SHA256SRI ranks lowest of all — it is a wrapper, not an algorithm,
so resolve it with assertedAlg before ranking; ranking below every real algorithm, an
unresolved SRI never wins a strongest-digest comparison.
>>>integrityStrength SHA512 > integrityStrength SHA256True
>>>integrityStrength SHA1 >= integrityStrength SHA256False
assertedAlg :: Hash -> Maybe HashAlg Source #
The algorithm a Hash asserts: its tag directly, or — for an SRI string — the
algorithm named in its <alg>-<base64> prefix. The SRI prefixes resolved are
sha256, sha384 and sha512 (every long digest the model represents and a registry
serves); an unrecognised or malformed prefix yields Nothing, so it asserts no
algorithm and clears no floor (the fail-closed reading).
>>>import Ecluse.Package (mkHash, HashAlg (SHA1, SRI))>>>assertedAlg <$> mkHash SRI "sha512-z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg=="Right (Just SHA512)
>>>assertedAlg <$> mkHash SHA1 "da39a3ee5e6b4b0d3255bfef95601890afd80709"Right (Just SHA1)
>>>assertedAlg <$> mkHash SRI "sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb"Right (Just SHA384)
Algorithm names and SRI strings
renderHashAlg :: HashAlg -> Text Source #
The lower-case wire name of an algorithm — the canonical spelling parseHashAlg
reads back. Total and injective, so it doubles as config rendering and error text.
>>>renderHashAlg SHA256"sha256"
parseHashAlg :: Text -> Either Text HashAlg Source #
Parse an algorithm name, tolerating case and an optional internal '-' (so
"SHA-256" and "sha256" both parse). An unrecognised name is reported as such,
distinct from a recognised-but-too-weak floor. This admits only the named hash
algorithms; the sri wrapper is not a config-selectable algorithm and is rejected.
>>>parseHashAlg "SHA-256"Right SHA256
>>>parseHashAlg "frobnicate"Left "unknown integrity algorithm: frobnicate"
sriAlgorithm :: Text -> Maybe HashAlg Source #
The HashAlg a Subresource-Integrity string names, read from its <alg> prefix.
The prefixes resolved are the Subresource-Integrity set sha256, sha384 and sha512
(every long digest the model represents and a registry serves); an unrecognised or
malformed prefix yields Nothing, so the string asserts no algorithm and clears no
floor (the fail-closed reading).
>>>sriAlgorithm "sha512-Zm9vYmFy"Just SHA512
>>>sriAlgorithm "sha384-Zm9vYmFy"Just SHA384
sriPrefix :: Text -> Text Source #
The algorithm-name token of a Subresource-Integrity string — the <alg> before
the first '-' in <alg>-<base64>. A string with no '-' is all prefix.
>>>sriPrefix "sha512-Zm9vYmFy""sha512"
sriBody :: Text -> Text Source #
The base64 digest body of a Subresource-Integrity string — the <base64> after
the first '-' in <alg>-<base64>. A string with no '-' has an empty body.
>>>sriBody "sha512-Zm9vYmFy""Zm9vYmFy"
The public-integrity floor
data MinIntegrity Source #
The configured minimum integrity algorithm a public (untrusted) version's
digest must meet to be admitted. Opaque and hard-floored at SHA-256: build it
only through mkMinIntegrity / parseMinIntegrity, which reject anything weaker, so
a value of this type carries the proof that the floor is itself collision-resistant.
Instances
| Show MinIntegrity Source # | |
Defined in Ecluse.Package.Integrity Methods showsPrec :: Int -> MinIntegrity -> ShowS # show :: MinIntegrity -> String # showList :: [MinIntegrity] -> ShowS # | |
| Eq MinIntegrity Source # | |
Defined in Ecluse.Package.Integrity | |
defaultMinIntegrity :: MinIntegrity Source #
The default public-integrity floor: SHA-256, which is also the hard minimum the floor may never be set below.
mkMinIntegrity :: HashAlg -> Either Text MinIntegrity Source #
Build a MinIntegrity, rejecting any algorithm weaker than SHA-256 (the hard
floor). A weak floor is a configuration error, never a silent clamp: a public version
admitted on a SHA-1 digest could be substituted by a collision, defeating the gate.
parseMinIntegrity :: Text -> Either Text MinIntegrity Source #
Parse a MinIntegrity from an algorithm name (e.g. "sha256", "sha512",
"blake2b"), case- and separator-insensitive. An unrecognised name and an
algorithm below the SHA-256 floor are distinct errors, so a misconfiguration is
reported precisely.
unMinIntegrity :: MinIntegrity -> HashAlg Source #
The floor algorithm.
renderMinIntegrity :: MinIntegrity -> Text Source #
Render a MinIntegrity as its lower-case algorithm name (round-trips parseMinIntegrity).
meetsFloor :: MinIntegrity -> HashAlg -> Bool Source #
Whether an algorithm meets the floor: at least as strong as the configured
minimum. The candidate algorithm is a resolved one (from assertedAlg), never a
bare SRI.
Version admissibility
data VersionIntegrity Source #
How a version's artifacts stand against the public-integrity floor — the three-way verdict the public admission gate acts on.
Constructors
| MeetsFloor | At least one digest asserts an algorithm at or above the floor: admissible. |
| BelowFloor | The version carries an integrity digest, but none meets the floor (e.g. a legacy SHA-1 shasum only). Inadmissible from a public upstream — distinct from carrying no digest at all, so the refusal can say which. |
| NoIntegrity | The version carries no integrity digest of any kind: inadmissible. |
Instances
| Show VersionIntegrity Source # | |
Defined in Ecluse.Package.Integrity Methods showsPrec :: Int -> VersionIntegrity -> ShowS # show :: VersionIntegrity -> String # showList :: [VersionIntegrity] -> ShowS # | |
| Eq VersionIntegrity Source # | |
Defined in Ecluse.Package.Integrity Methods (==) :: VersionIntegrity -> VersionIntegrity -> Bool # (/=) :: VersionIntegrity -> VersionIntegrity -> Bool # | |
classifyArtifacts :: MinIntegrity -> NonEmpty Artifact -> VersionIntegrity Source #
Classify a version's artifacts against the floor. A version MeetsFloor iff any
of its digests (across all of its artifacts) asserts a floor-clearing algorithm;
failing that, it is NoIntegrity when no artifact carries any digest at all, else
BelowFloor. npm publishes one artifact per version, but the check spans the whole
NonEmpty so it holds for a multi-artifact ecosystem too.