mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-11 00:29:15 -05:00
npm versions published before dist.integrity existed carry only a SHA-1 dist.shasum, and Composer metadata lists only a SHA-1 dist.shasum, so neither could be checked. - Accept native integrity given as an algorithm-prefixed hex checksum, verified with integrity's exact-digest check, so a recorded "sha1-<hex>" is also checked on cache hits - npm: fall back to dist.shasum when dist.integrity is missing or cannot be parsed; a usable dist.integrity still takes precedence - Composer: verify an archive against its dist.shasum, except another commit of a branch fetched in place of the listed one, which the listed shasum does not describe - Fetch unverified as before when no shasum is listed, as Packagist does for GitHub archives, or it is not a SHA-1 digest
222 lines
5.9 KiB
Go
222 lines
5.9 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/git-pkgs/integrity"
|
|
)
|
|
|
|
type integrityChecks struct {
|
|
contentHash integrity.SRI
|
|
native integrity.SRI
|
|
// checksum is native metadata given as a single algorithm-prefixed hex
|
|
// checksum, such as a registry's SHA-1 shasum, which is not SRI.
|
|
checksum integrity.Digest
|
|
hasChecksum bool
|
|
algorithms []integrity.Algorithm
|
|
}
|
|
|
|
func newIntegrityChecks(contentHash, native string) (integrityChecks, error) {
|
|
checks := integrityChecks{}
|
|
|
|
if contentHash != "" {
|
|
digest, err := integrity.ParseHex(integrity.SHA256, contentHash)
|
|
if err != nil {
|
|
return integrityChecks{}, fmt.Errorf("parse content_hash: %w", err)
|
|
}
|
|
checks.contentHash = integrity.SRI{digest}
|
|
checks.algorithms = append(checks.algorithms, integrity.SHA256)
|
|
}
|
|
|
|
if native != "" {
|
|
digests, err := integrity.ParseSRI(native)
|
|
if err != nil {
|
|
checksum, checksumErr := integrity.ParseChecksum(native)
|
|
if checksumErr != nil {
|
|
return integrityChecks{}, fmt.Errorf("parse integrity: %w", err)
|
|
}
|
|
checks.checksum, checks.hasChecksum = checksum, true
|
|
checks.algorithms = append(checks.algorithms, checksum.Algorithm())
|
|
return checks, nil
|
|
}
|
|
checks.native = digests
|
|
for _, digest := range digests {
|
|
checks.algorithms = append(checks.algorithms, digest.Algorithm())
|
|
}
|
|
}
|
|
|
|
return checks, nil
|
|
}
|
|
|
|
// verifyIntegrity wraps body to check it against the declared integrity as
|
|
// it is read. verified, called once body has been read, reports a mismatch as
|
|
// ErrArtifactDigestMismatch, and also a body that was not read to its end,
|
|
// since only a complete body can be checked. An empty declared checks
|
|
// nothing.
|
|
func verifyIntegrity(body io.ReadCloser, declared string) (_ io.Reader, verified func() error, _ error) {
|
|
if declared == "" {
|
|
return body, func() error { return nil }, nil
|
|
}
|
|
checks, err := newIntegrityChecks("", declared)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
var mismatch string
|
|
reader, err := checks.newVerifyingReader(body, func(reason string) { mismatch = reason }, false)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return reader, func() error {
|
|
if !reader.(*verifyingReader).verified {
|
|
return fmt.Errorf("%w: body was not read to its end", ErrArtifactDigestMismatch)
|
|
}
|
|
if mismatch != "" {
|
|
return fmt.Errorf("%w: %s", ErrArtifactDigestMismatch, mismatch)
|
|
}
|
|
return nil
|
|
}, nil
|
|
}
|
|
|
|
func (c integrityChecks) wrap(source io.ReadCloser, onMismatch func(string)) (io.ReadCloser, error) {
|
|
if len(c.algorithms) == 0 {
|
|
return source, nil
|
|
}
|
|
verified, err := c.newVerifyingReader(source, onMismatch, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
seeker, ok := source.(io.Seeker)
|
|
if !ok {
|
|
return verified, nil
|
|
}
|
|
return &seekableVerifyingReader{
|
|
source: source,
|
|
verified: verified,
|
|
seeker: seeker,
|
|
}, nil
|
|
}
|
|
|
|
// wrapFailOnMismatch is wrap for bytes that have not been checked anywhere
|
|
// else: a mismatch is also returned from Read as ErrArtifactDigestMismatch in
|
|
// place of io.EOF, so the caller can abort rather than complete the response.
|
|
func (c integrityChecks) wrapFailOnMismatch(source io.ReadCloser, onMismatch func(string)) (io.ReadCloser, error) {
|
|
return c.newVerifyingReader(source, onMismatch, true)
|
|
}
|
|
|
|
func (c integrityChecks) newVerifyingReader(source io.ReadCloser, onMismatch func(string), failOnMismatch bool) (io.ReadCloser, error) {
|
|
if len(c.algorithms) == 0 {
|
|
return source, nil
|
|
}
|
|
reader, err := integrity.NewReader(source, c.algorithms...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create integrity reader: %w", err)
|
|
}
|
|
return &verifyingReader{
|
|
source: source,
|
|
reader: reader,
|
|
checks: c,
|
|
onMismatch: onMismatch,
|
|
failOnMismatch: failOnMismatch,
|
|
}, nil
|
|
}
|
|
|
|
// verifyingReader forwards Close to its source and reports completed digest
|
|
// mismatches after its shared integrity reader observes EOF.
|
|
type verifyingReader struct {
|
|
source io.ReadCloser
|
|
reader *integrity.Reader
|
|
checks integrityChecks
|
|
onMismatch func(reason string)
|
|
verified bool
|
|
|
|
failOnMismatch bool
|
|
mismatched bool
|
|
}
|
|
|
|
// seekableVerifyingReader verifies ordinary reads until a successful seek,
|
|
// after which reads bypass whole-object verification for range responses.
|
|
type seekableVerifyingReader struct {
|
|
source io.ReadCloser
|
|
verified io.ReadCloser
|
|
seeker io.Seeker
|
|
bypass bool
|
|
}
|
|
|
|
func (r *seekableVerifyingReader) Read(p []byte) (int, error) {
|
|
if r.bypass {
|
|
return r.source.Read(p)
|
|
}
|
|
return r.verified.Read(p)
|
|
}
|
|
|
|
func (r *seekableVerifyingReader) Seek(offset int64, whence int) (int64, error) {
|
|
position, err := r.seeker.Seek(offset, whence)
|
|
if err == nil {
|
|
r.bypass = true
|
|
}
|
|
return position, err
|
|
}
|
|
|
|
func (r *seekableVerifyingReader) Close() error {
|
|
return r.verified.Close()
|
|
}
|
|
|
|
func (r *verifyingReader) Read(p []byte) (int, error) {
|
|
n, err := r.reader.Read(p)
|
|
if err == io.EOF {
|
|
r.verify()
|
|
if r.failOnMismatch && r.mismatched {
|
|
return n, ErrArtifactDigestMismatch
|
|
}
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
func (r *verifyingReader) Close() error {
|
|
return r.source.Close()
|
|
}
|
|
|
|
func (r *verifyingReader) verify() {
|
|
if r.verified {
|
|
return
|
|
}
|
|
r.verified = true
|
|
result := r.reader.Result()
|
|
if !result.Complete {
|
|
return
|
|
}
|
|
|
|
if len(r.checks.contentHash) > 0 {
|
|
if err := result.Verify(r.checks.contentHash); err != nil {
|
|
r.mismatched = true
|
|
r.onMismatch("content_hash: " + err.Error())
|
|
}
|
|
}
|
|
if len(r.checks.native) > 0 {
|
|
if err := result.Verify(r.checks.native); err != nil {
|
|
r.mismatched = true
|
|
r.onMismatch("integrity: " + err.Error())
|
|
}
|
|
}
|
|
if r.checks.hasChecksum {
|
|
if err := result.VerifyDigest(r.checks.checksum); err != nil {
|
|
r.mismatched = true
|
|
r.onMismatch("integrity: " + err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
// sha1Checksum returns a registry's hexadecimal SHA-1 shasum as the
|
|
// "sha1-<hex>" checksum newIntegrityChecks accepts, or "" when it is empty or
|
|
// not a SHA-1 digest.
|
|
func sha1Checksum(shasum string) string {
|
|
if shasum == "" {
|
|
return ""
|
|
}
|
|
digest, err := integrity.ParseHex(integrity.SHA1, shasum)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return digest.Checksum()
|
|
}
|