Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ FUZZTIME ?= 30s
fuzz:
go test -fuzz=FuzzParseSRI -fuzztime=$(FUZZTIME) .
go test -fuzz=FuzzSRIRoundTrip -fuzztime=$(FUZZTIME) .
go test -fuzz=FuzzParseChecksum -fuzztime=$(FUZZTIME) .
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# integrity

Parse Subresource Integrity metadata and verify package streams while they are read. The module supports SHA-256, SHA-384, and SHA-512 using only the Go standard library.
Parse Subresource Integrity metadata and registry checksums, and verify package streams while they are read. The module supports SHA-256, SHA-384, and SHA-512 using only the Go standard library, and MD5 and SHA-1 for verifying checksums registries already publish.

## Install

Expand Down Expand Up @@ -75,6 +75,32 @@ SRI verification uses the strongest supported algorithm in the metadata list. An

The caller must close the source and keep copied bytes private until verification succeeds. Write cache entries to a temporary file or object, then commit them after `Verify` returns nil. Reporting failures and cache policy remain with the caller.

## Verify a registry checksum

Many registries publish a single hexadecimal checksum rather than SRI, sometimes in MD5 or SHA-1: npm's `dist.shasum`, Packagist's `dist.shasum`, Conda's `md5`, or a Vagrant box's `checksum_type` and `checksum`. `ParseChecksum` parses an algorithm-prefixed value such as `sha256-<hex>` or `sha1:<hex>`. `ParseHex` takes the algorithm separately. `Result.VerifyDigest` checks a completed stream against that one exact digest.

```go
expected, err := integrity.ParseHex(integrity.SHA1, shasum)
if err != nil {
return err
}

reader, err := integrity.NewReader(response.Body, expected.Algorithm())
if err != nil {
return err
}
if _, err := io.Copy(temporary, reader); err != nil {
return err
}
if err := reader.Result().VerifyDigest(expected); err != nil {
return err
}
```

MD5 and SHA-1 are compatibility checks for metadata a publisher already supplied. Both are broken for collision resistance, so prefer a stronger checksum whenever the registry offers one, and never choose them for new integrity metadata. They are not SRI algorithms: `ParseSRI` rejects them, `Digest.SRI` returns an empty string for them, `FormatSRI` leaves them out, and `Result.Verify` refuses an SRI list containing them. `Digest.Checksum` gives the canonical `<algorithm>-<hex>` form for any algorithm.

SRI verification ranks algorithms by an explicit strength order (SHA-512, then SHA-384, then SHA-256), not by the order of the `Algorithm` constants.

## Development

Run the tests and race detector:
Expand All @@ -94,6 +120,7 @@ Run each fuzz target:
```bash
go test -fuzz=FuzzParseSRI -fuzztime=30s
go test -fuzz=FuzzSRIRoundTrip -fuzztime=30s
go test -fuzz=FuzzParseChecksum -fuzztime=30s
```

Run the benchmarks:
Expand Down
61 changes: 60 additions & 1 deletion algorithm.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package integrity

import (
"fmt"
"slices"
"strconv"
"strings"
)
Expand All @@ -16,15 +17,26 @@ const (
SHA384
// SHA512 identifies SHA-512 digests.
SHA512
// MD5 identifies MD5 digests. It is a compatibility check for checksums a
// publisher already supplied and is not a Subresource Integrity
// algorithm. Do not choose it for new integrity metadata.
MD5
// SHA1 identifies SHA-1 digests. It is a compatibility check for
// checksums a publisher already supplied and is not a Subresource
// Integrity algorithm. Do not choose it for new integrity metadata.
SHA1
)

const (
md5Size = 16
sha1Size = 20
sha256Size = 32
sha384Size = 48
sha512Size = 64
)

// String returns the lower-case SRI name for the algorithm.
// String returns the lower-case name for the algorithm, as SRI and
// algorithm-prefixed checksums spell it.
func (a Algorithm) String() string {
switch a {
case SHA256:
Expand All @@ -33,11 +45,38 @@ func (a Algorithm) String() string {
return "sha384"
case SHA512:
return "sha512"
case MD5:
return "md5"
case SHA1:
return "sha1"
default:
return "Algorithm(" + strconv.FormatUint(uint64(a), 10) + ")"
}
}

// sri reports whether the algorithm may appear in Subresource Integrity
// metadata.
func (a Algorithm) sri() bool {
switch a {
case SHA256, SHA384, SHA512:
return true
default:
return false
}
}

// byStrength lists the algorithms from weakest to strongest. SRI
// verification ranks by it to pick the strongest algorithm, rather than by
// the order of the constants, which reflects only when each was added.
var byStrength = []Algorithm{MD5, SHA1, SHA256, SHA384, SHA512}

// strength ranks an algorithm by its place in byStrength, from 1 for the
// weakest; it is 0 for an unsupported algorithm.
func (a Algorithm) strength() int {
return slices.Index(byStrength, a) + 1
}

// parseAlgorithm parses any supported algorithm name.
func parseAlgorithm(value string) (Algorithm, error) {
if len(value) > len("sha512") {
return 0, fmt.Errorf("unsupported algorithm")
Expand All @@ -49,13 +88,33 @@ func parseAlgorithm(value string) (Algorithm, error) {
return SHA384, nil
case "sha512":
return SHA512, nil
case "md5":
return MD5, nil
case "sha1":
return SHA1, nil
default:
return 0, fmt.Errorf("unsupported algorithm %q", value)
}
}

// parseSRIAlgorithm parses the name of an algorithm SRI metadata may use.
func parseSRIAlgorithm(value string) (Algorithm, error) {
algorithm, err := parseAlgorithm(value)
if err != nil {
return 0, err
}
if !algorithm.sri() {
return 0, fmt.Errorf("%s is not a Subresource Integrity algorithm", algorithm)
}
return algorithm, nil
}

func digestSize(algorithm Algorithm) (int, error) {
switch algorithm {
case MD5:
return md5Size, nil
case SHA1:
return sha1Size, nil
case SHA256:
return sha256Size, nil
case SHA384:
Expand Down
Loading
Loading