Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: implement signing of extensions #79

Merged
merged 22 commits into from
Dec 13, 2024
Merged

Conversation

Emyrk
Copy link
Member

@Emyrk Emyrk commented Dec 11, 2024

What this does

Signs extensions so that this warning goes away. Will close #65

Screenshot From 2024-12-12 13-30-36

How it does this.

  • On add, the plaintext payload to sign is generated.
  • Use --sign to have the marketplace sign extensions, without this flag, the marketplace acts as it used to.
  1. Manifest includes signed extension dynamically:
    manifest.Assets.Asset = append(manifest.Assets.Asset, VSIXAsset{
    Type: VSIXSignatureType,
    Path: sigzipFilename,
    Addressable: "true",
    })
  2. Payload is signed dynamically on demand:
    if s.SigningEnabled() && filepath.Base(fp) == "p7s.sig" {
    // This file must exist, and it is always empty
    return mem.NewFileHandle(mem.CreateFile("p7s.sig")), nil
    }
    if s.SigningEnabled() && filepath.Base(fp) == sigzipFilename {
    // hijack this request, sign the sig manifest
    manifest, err := s.Storage.Open(ctx, filepath.Join(filepath.Dir(fp), sigManifestName))
    if err != nil {
    fmt.Println(err)
    return nil, xerrors.Errorf("open signature manifest: %w", err)
    }
    defer manifest.Close()
    manifestData, err := io.ReadAll(manifest)
    if err != nil {
    return nil, xerrors.Errorf("read signature manifest: %w", err)
    }
    signed, err := extensionsign.SignAndZipManifest(s.Signer, manifestData)
    if err != nil {
    return nil, xerrors.Errorf("sign and zip manifest: %w", err)
    }
    f := mem.NewFileHandle(mem.CreateFile(sigzipFilename))
    _, err = f.Write(signed)
    return f, err
    }

This means no signed values ever hit the storage.

Work still needed

This implementation is MVP. It still needs to handle importing the signing key, and handling existing extensions. As the signature payload is generated on add. All of this will be handled in a follow up PR.

Refactors

  • FileServer used to return a http.Handler. I changed this to Open(ctx context.Context, name string) (fs.File, error). This makes the storage closer to a fs.FS, meaning I could implement Signatures as a wrapper. Rather than having to mutate stores.
  • Command server flags are defined in 1 place, and used throughout.

Unfortunate

  • This change requires the file to be fully read from network before serving from Artifactory. Do we think keeping these bytes in memory is an issue? Rather than streaming?
    • Keep in mind for signatures to work, the payload needs to be signed, so it has to enter memory. Just unfortunate it always needs to, even when not using signatures.
    • This is because I am using afero for mem files, and do not see a way to stream.

@Emyrk Emyrk changed the title chore: begin work implementing vsix signatures, manifest matching chore: implement signing of extensions Dec 12, 2024
@Emyrk Emyrk force-pushed the stevenmasley/vsix_signatures branch from 166a21d to 21661f0 Compare December 12, 2024 19:11
@Emyrk Emyrk marked this pull request as ready for review December 12, 2024 19:45
Comment on lines 270 to 277
for _, file := range extra {
// TODO: I think this is correct?
_, err := s.upload(ctx, path.Join(dir, file.RelativePath), bytes.NewReader(file.Content))
if err != nil {
return "", err
}
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That looks right to me!

@Emyrk Emyrk requested a review from code-asher December 12, 2024 20:55
Copy link
Member

@code-asher code-asher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking comments, it looks great! I have not had a chance to run this with code-server or anything yet, and I definitely will, but no need to block on me doing that I think.

repo string
)

addFlags, opts := serverFlags()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oo yeah very nice

cli/server.go Outdated Show resolved Hide resolved
cmd.Flags().StringVar(&opts.Artifactory, "artifactory", "", "Artifactory server URL.")
cmd.Flags().StringVar(&opts.Repo, "repo", "", "Artifactory repository.")
cmd.Flags().DurationVar(&opts.ListCacheDuration, "list-cache-duration", time.Minute, "The duration of the extension cache.")
cmd.Flags().BoolVar(&sign, "sign", false, "Sign extensions.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort of a similar thought process here, I know it is hidden at the moment, but remove with --sign might not make sense, I think? Also not sure about add, still need to go through the rest of the PR, but we are signing on demand right? So only with server? I am assuming we just always create the signature manifest on add.

Mostly thinking of it in terms of what a user might see and think when they run subcommand --help.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right it does not make the most sense, but it is hidden. The sign flag will be removed though, replaced by something that will parse a key or cert.

See the flags in my other PR:

cmd.Flags().StringArrayVar(&certificates, "certs", []string{}, "The path to certificates that match the signing key.")
cmd.Flags().StringVar(&signingKeyFile, "key", "", "The path to signing key file in PEM format.")
cmd.Flags().BoolVar(&opts.SaveSigZips, "save-sigs", false, "Save signed extensions to disk for debugging.")

I do have a flag to save the signature to disk. Makes it easier to debug

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to leave sign for now. I intend to fix it before being unhidden, anyone using it will be broken when I change things.

When I fix it, I'll make the flags make sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fabulous!

Comment on lines 270 to 277
for _, file := range extra {
// TODO: I think this is correct?
_, err := s.upload(ctx, path.Join(dir, file.RelativePath), bytes.NewReader(file.Content))
if err != nil {
return "", err
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That looks right to me!

storage/artifactory.go Outdated Show resolved Hide resolved
storage/local.go Outdated Show resolved Hide resolved
})
}

f := mem.NewFileHandle(mem.CreateFile(fp))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly a stupid question, and I have not seen the rest of the usage yet, but would it make sense to return the reader to avoid having to hold the files in memory? Then the caller can decide to read it in if necessary (for signing, which then returns a reader for the sigzip after doing its thing) or just stream it as usual for any other file.

Edit: nvm, I see how this makes it easier because it mirrors the local one where we can just use http.Dir().Open() which is nice.

Or...is it possible to implement a File that Reads from the HTTP response? Not sure how Stat would be implemented though. In any case, probably not a big deal. The biggest file is probably the .vsix, and surely there are not multi-gigabyte .vsix files I would hope.

Although, could it maybe be an issue for large deployments serving many files to many people at once?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was really wondering about this too, but ended up not spending the time to try to fix it.

Or...is it possible to implement a File that Reads from the HTTP response? Not sure how Stat would be implemented though. In any case, probably not a big deal. The biggest file is probably the .vsix, and surely there are not multi-gigabyte .vsix files I would hope.

This would be ideal, you can implement Stat rather easily, it's implementing the io.Seeker that was problematic when I spent a few minutes on it.

I think we should revisit this and try to fix it. I'll throw on a comment. If we re-implement http.FileServer, we might be able to make our own easier to implement interface too. Ignore things like Stat

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, that sounds good to me.

storage/signature.go Outdated Show resolved Hide resolved
storage/storage_test.go Outdated Show resolved Hide resolved
storage/artifactory.go Show resolved Hide resolved
Copy link
Member Author

Emyrk commented Dec 13, 2024

This stack of pull requests is managed by Graphite. Learn more about stacking.

@Emyrk Emyrk merged commit f35db41 into main Dec 13, 2024
6 checks passed
@Emyrk Emyrk deleted the stevenmasley/vsix_signatures branch December 13, 2024 16:52
@github-actions github-actions bot locked and limited conversation to collaborators Dec 13, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

All extensions show as unsigned by the Extension Marketplace preventing auto installation in VSCode 1.9.4+
2 participants