ecluse:ecluse-core
Safe HaskellNone
LanguageGHC2021

Ecluse.Core.Package.Hash

Description

The integrity-digest vocabulary: hash algorithms and their authority order, the validated Hash value, digest computation, and the Subresource-Integrity wire forms.

This is the single home for the algorithm vocabulary: the wire name an algorithm renders to and parses from, and how a Subresource-Integrity string is split and resolved. It lives in the package layer's lowest module because mkHash needs it and the vocabulary's consumers must never disagree on it: everything that names an algorithm or reads an SRI (the worker's tamper gate, the serve-admission floor, the queue wire) defers here, so they share one notion of what "sha512" means and what an SRI asserts rather than each re-encoding it. Ecluse.Core.Package re-exports this whole surface for its callers; import this module directly only where the package vocabulary itself is not needed.

Synopsis

Hashes

data Hash Source #

An integrity digest of an artifact. Opaque: a Hash is built only through mkHash, which validates that the digest is well-formed, so every value of this type carries the proof that its digest could be a real digest of its algorithm. Read it back through hashAlg and hashValue.

Instances

Instances details
Show Hash Source # 
Instance details

Defined in Ecluse.Core.Package.Hash

Methods

showsPrec :: Int -> Hash -> ShowS #

show :: Hash -> String #

showList :: [Hash] -> ShowS #

Eq Hash Source # 
Instance details

Defined in Ecluse.Core.Package.Hash

Methods

(==) :: Hash -> Hash -> Bool #

(/=) :: Hash -> Hash -> Bool #

hashAlg :: Hash -> HashAlg Source #

The algorithm the digest was computed with.

hashValue :: Hash -> Text Source #

The digest itself, in the algorithm's wire encoding (e.g. hex, or the single sha512-… component for SRI).

mkHash :: HashAlg -> Text -> Either Text Hash Source #

Build a Hash, validating that the digest is structurally well-formed: cleanly encoded and exactly the byte length its algorithm specifies. This is the only way to construct a Hash, so the type itself is the proof that the digest could be a real digest of that algorithm -- an empty, truncated, over-long, non-hex, or bad-base64 value is unconstructable and so can never reach an integrity gate as a degenerate digest (the fail-open this closes is docs/architecture/security.md invariant 5).

Well-formedness is not admissibility: a well-formed but weak SHA-1 digest builds fine; whether it clears the public-integrity floor is the separate decision of Ecluse.Core.Package.Integrity. mkHash rejects a malformed digest, never a merely weak one.

A hex-tagged algorithm (everything but SRI) takes lower- or upper-case hex of the algorithm's digest length. An SRI takes exactly one <alg>-<base64> component, naming a Subresource-Integrity algorithm (sha256, sha384, sha512) whose base64 body decodes to that algorithm's digest length. A wire string that joins several components with whitespace is malformed here: split it with mkSriHashes, which yields one Hash per component, so no reader ever has to decide which component of a joined string a Hash means.

>>> import Ecluse.Core.Package.Hash (HashAlg (SHA1))
>>> fmap hashAlg (mkHash SHA1 "0a4d55a8d778e5022fab701977c5d840bbc486d0")
Right SHA1
>>> mkHash SHA1 "deadbeef"
Left "malformed sha1 digest"

mkSriHashes :: Text -> Either Text (NonEmpty Hash) Source #

Split a Subresource-Integrity wire string -- one or more whitespace-separated <alg>-<base64> components (npm's dist.integrity) -- into one SRI Hash per component, each built through the validating mkHash. The whole string is rejected when it carries no component or any component is malformed, so a partially-valid value never yields a partial digest set.

This is the one intended path from wire data to SRI hashes. Because each resulting Hash holds exactly one component, the admission floor, the worker's tamper gate, and the divergence fingerprint all resolve the same algorithm and digest body from it -- there is no joined string left for two consumers to read two different ways.

>>> fmap length (mkSriHashes "sha512-z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg== sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=")
Right 2
>>> mkSriHashes "  "
Left "malformed sri digest"

data HashAlg Source #

A hash algorithm an integrity digest is computed with.

The Ord instance is the integrity authority order, not constructor order: SRI < MD5 < SHA1 < SHA256 < SHA384 < Blake2b < SHA512. A bare SRI is a wrapper, not an algorithm; callers that care about its embedded algorithm should resolve it first.

Constructors

SHA1 
SHA256 
SHA384 
SHA512 
MD5 
Blake2b 
SRI

A single Subresource-Integrity component (npm dist.integrity), e.g. "sha512-…". Exactly one <alg>-<base64> component per Hash: a wire string that joins several with whitespace is split into one Hash per component by mkSriHashes, so every reader resolves the same algorithm and digest body from hashValue.

Instances

Instances details
Generic HashAlg Source # 
Instance details

Defined in Ecluse.Core.Package.Hash

Associated Types

type Rep HashAlg 
Instance details

Defined in Ecluse.Core.Package.Hash

type Rep HashAlg = D1 ('MetaData "HashAlg" "Ecluse.Core.Package.Hash" "ecluse-0.1.0-inplace-ecluse-core" 'False) ((C1 ('MetaCons "SHA1" 'PrefixI 'False) (U1 :: Type -> Type) :+: (C1 ('MetaCons "SHA256" 'PrefixI 'False) (U1 :: Type -> Type) :+: C1 ('MetaCons "SHA384" 'PrefixI 'False) (U1 :: Type -> Type))) :+: ((C1 ('MetaCons "SHA512" 'PrefixI 'False) (U1 :: Type -> Type) :+: C1 ('MetaCons "MD5" 'PrefixI 'False) (U1 :: Type -> Type)) :+: (C1 ('MetaCons "Blake2b" 'PrefixI 'False) (U1 :: Type -> Type) :+: C1 ('MetaCons "SRI" 'PrefixI 'False) (U1 :: Type -> Type))))

Methods

from :: HashAlg -> Rep HashAlg x #

to :: Rep HashAlg x -> HashAlg #

Show HashAlg Source # 
Instance details

Defined in Ecluse.Core.Package.Hash

Eq HashAlg Source # 
Instance details

Defined in Ecluse.Core.Package.Hash

Methods

(==) :: HashAlg -> HashAlg -> Bool #

(/=) :: HashAlg -> HashAlg -> Bool #

Ord HashAlg Source # 
Instance details

Defined in Ecluse.Core.Package.Hash

Universe HashAlg Source # 
Instance details

Defined in Ecluse.Core.Package.Hash

Methods

universe :: [HashAlg] Source #

type Rep HashAlg Source # 
Instance details

Defined in Ecluse.Core.Package.Hash

type Rep HashAlg = D1 ('MetaData "HashAlg" "Ecluse.Core.Package.Hash" "ecluse-0.1.0-inplace-ecluse-core" 'False) ((C1 ('MetaCons "SHA1" 'PrefixI 'False) (U1 :: Type -> Type) :+: (C1 ('MetaCons "SHA256" 'PrefixI 'False) (U1 :: Type -> Type) :+: C1 ('MetaCons "SHA384" 'PrefixI 'False) (U1 :: Type -> Type))) :+: ((C1 ('MetaCons "SHA512" 'PrefixI 'False) (U1 :: Type -> Type) :+: C1 ('MetaCons "MD5" 'PrefixI 'False) (U1 :: Type -> Type)) :+: (C1 ('MetaCons "Blake2b" 'PrefixI 'False) (U1 :: Type -> Type) :+: C1 ('MetaCons "SRI" 'PrefixI 'False) (U1 :: Type -> Type))))

Algorithm vocabulary

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 surrounding whitespace and case, and a single family-separating '-' (so "SHA-256" and "sha256" both parse). It accepts only the canonical names and their documented single-dash aliases: it does not strip arbitrary internal dashes, so a typo such as "s-h-a--2-5-6" is rejected rather than silently read as sha256. An unrecognised name is reported as such, distinct from a recognised-but-too-weak floor. The sri wrapper is not a config-selectable algorithm and is rejected.

>>> parseHashAlg "SHA-256"
Right SHA256
>>> parseHashAlg "frobnicate"
Left "unknown integrity algorithm: frobnicate"

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"

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

Digest computation

computeDigest :: HashAlg -> Maybe (LByteString -> ByteString) Source #

Compute the digest of bytes in a given algorithm, as the raw digest bytes, or Nothing for an algorithm Écluse will not verify against. The computable algorithms are exactly the collision-resistant ones: SHA1, SHA256, SHA384, SHA512, and Blake2b-512. MD5 is deliberately uncomputable here (a match on a broken hash cannot prove the bytes were not substituted, so the tamper gate never verifies against it), as is the bare SRI wrapper, which names no algorithm of its own (resolve it with sriAlgorithm first).

This is the sibling of hexDigestOk: both dispatch on the same per-algorithm crypto type, so they live together and a new HashAlg must be given an arm in each (the 'case' is total, and the package builds with -Wincomplete-patterns as an error). It is the one place that defines which algorithms the worker can verify; the integrity floor admits by strength (Ecluse.Core.Package.Integrity), and the invariant that every floor-clearing algorithm is computable here keeps the worker able to verify whatever the floor admits.

isComputable :: HashAlg -> Bool Source #

Whether the worker can compute (and so verify a digest in) the given algorithm: the predicate form of computeDigest, taken from the same single definition so the computable set cannot drift from what computeDigest actually computes.

>>> isComputable SHA256
True
>>> isComputable MD5
False