mirror of https://github.com/k3d-io/k3d
feat: fetch K3s image tag from channelserver on latest/stable/+min.major (#841)
parent
a1a04b7907
commit
b073e7675f
@ -0,0 +1,54 @@ |
|||||||
|
/* |
||||||
|
Copyright © 2020-2021 The k3d Author(s) |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in |
||||||
|
all copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||||
|
THE SOFTWARE. |
||||||
|
*/ |
||||||
|
package k3s |
||||||
|
|
||||||
|
const K3sChannelServerURL string = "https://update.k3s.io/v1-release/channels" |
||||||
|
|
||||||
|
// Source: https://github.com/rancher/channelserver/blob/b084228bfc14d280e524ad132311d8cfcebd1271/pkg/model/config.go#L15-L20
|
||||||
|
type Channel struct { |
||||||
|
Name string `json:"name,omitempty"` |
||||||
|
Latest string `json:"latest,omitempty"` |
||||||
|
LatestRegexp string `json:"latestRegexp,omitempty"` |
||||||
|
ExcludeRegexp string `json:"excludeRegexp,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
type ChannelServerResponse struct { |
||||||
|
Channels []struct { |
||||||
|
Channel `json:,squash` |
||||||
|
} `json:"data"` |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
{ |
||||||
|
|
||||||
|
"type": "collection", |
||||||
|
"links": { |
||||||
|
"self": "…/v1-release/channels" |
||||||
|
}, |
||||||
|
"actions": { }, |
||||||
|
"resourceType": "channels", |
||||||
|
"data": [ 13 items |
||||||
|
{ |
||||||
|
"id": "stable", |
||||||
|
"type": "channel", |
||||||
|
"links": { … }, |
||||||
|
*/ |
@ -1,287 +0,0 @@ |
|||||||
package schema1 |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"crypto/sha512" |
|
||||||
"encoding/json" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/docker/distribution" |
|
||||||
"github.com/docker/distribution/manifest" |
|
||||||
"github.com/docker/distribution/reference" |
|
||||||
"github.com/docker/libtrust" |
|
||||||
"github.com/opencontainers/go-digest" |
|
||||||
) |
|
||||||
|
|
||||||
type diffID digest.Digest |
|
||||||
|
|
||||||
// gzippedEmptyTar is a gzip-compressed version of an empty tar file
|
|
||||||
// (1024 NULL bytes)
|
|
||||||
var gzippedEmptyTar = []byte{ |
|
||||||
31, 139, 8, 0, 0, 9, 110, 136, 0, 255, 98, 24, 5, 163, 96, 20, 140, 88, |
|
||||||
0, 8, 0, 0, 255, 255, 46, 175, 181, 239, 0, 4, 0, 0, |
|
||||||
} |
|
||||||
|
|
||||||
// digestSHA256GzippedEmptyTar is the canonical sha256 digest of
|
|
||||||
// gzippedEmptyTar
|
|
||||||
const digestSHA256GzippedEmptyTar = digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4") |
|
||||||
|
|
||||||
// configManifestBuilder is a type for constructing manifests from an image
|
|
||||||
// configuration and generic descriptors.
|
|
||||||
type configManifestBuilder struct { |
|
||||||
// bs is a BlobService used to create empty layer tars in the
|
|
||||||
// blob store if necessary.
|
|
||||||
bs distribution.BlobService |
|
||||||
// pk is the libtrust private key used to sign the final manifest.
|
|
||||||
pk libtrust.PrivateKey |
|
||||||
// configJSON is configuration supplied when the ManifestBuilder was
|
|
||||||
// created.
|
|
||||||
configJSON []byte |
|
||||||
// ref contains the name and optional tag provided to NewConfigManifestBuilder.
|
|
||||||
ref reference.Named |
|
||||||
// descriptors is the set of descriptors referencing the layers.
|
|
||||||
descriptors []distribution.Descriptor |
|
||||||
// emptyTarDigest is set to a valid digest if an empty tar has been
|
|
||||||
// put in the blob store; otherwise it is empty.
|
|
||||||
emptyTarDigest digest.Digest |
|
||||||
} |
|
||||||
|
|
||||||
// NewConfigManifestBuilder is used to build new manifests for the current
|
|
||||||
// schema version from an image configuration and a set of descriptors.
|
|
||||||
// It takes a BlobService so that it can add an empty tar to the blob store
|
|
||||||
// if the resulting manifest needs empty layers.
|
|
||||||
func NewConfigManifestBuilder(bs distribution.BlobService, pk libtrust.PrivateKey, ref reference.Named, configJSON []byte) distribution.ManifestBuilder { |
|
||||||
return &configManifestBuilder{ |
|
||||||
bs: bs, |
|
||||||
pk: pk, |
|
||||||
configJSON: configJSON, |
|
||||||
ref: ref, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Build produces a final manifest from the given references
|
|
||||||
func (mb *configManifestBuilder) Build(ctx context.Context) (m distribution.Manifest, err error) { |
|
||||||
type imageRootFS struct { |
|
||||||
Type string `json:"type"` |
|
||||||
DiffIDs []diffID `json:"diff_ids,omitempty"` |
|
||||||
BaseLayer string `json:"base_layer,omitempty"` |
|
||||||
} |
|
||||||
|
|
||||||
type imageHistory struct { |
|
||||||
Created time.Time `json:"created"` |
|
||||||
Author string `json:"author,omitempty"` |
|
||||||
CreatedBy string `json:"created_by,omitempty"` |
|
||||||
Comment string `json:"comment,omitempty"` |
|
||||||
EmptyLayer bool `json:"empty_layer,omitempty"` |
|
||||||
} |
|
||||||
|
|
||||||
type imageConfig struct { |
|
||||||
RootFS *imageRootFS `json:"rootfs,omitempty"` |
|
||||||
History []imageHistory `json:"history,omitempty"` |
|
||||||
Architecture string `json:"architecture,omitempty"` |
|
||||||
} |
|
||||||
|
|
||||||
var img imageConfig |
|
||||||
|
|
||||||
if err := json.Unmarshal(mb.configJSON, &img); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
if len(img.History) == 0 { |
|
||||||
return nil, errors.New("empty history when trying to create schema1 manifest") |
|
||||||
} |
|
||||||
|
|
||||||
if len(img.RootFS.DiffIDs) != len(mb.descriptors) { |
|
||||||
return nil, fmt.Errorf("number of descriptors and number of layers in rootfs must match: len(%v) != len(%v)", img.RootFS.DiffIDs, mb.descriptors) |
|
||||||
} |
|
||||||
|
|
||||||
// Generate IDs for each layer
|
|
||||||
// For non-top-level layers, create fake V1Compatibility strings that
|
|
||||||
// fit the format and don't collide with anything else, but don't
|
|
||||||
// result in runnable images on their own.
|
|
||||||
type v1Compatibility struct { |
|
||||||
ID string `json:"id"` |
|
||||||
Parent string `json:"parent,omitempty"` |
|
||||||
Comment string `json:"comment,omitempty"` |
|
||||||
Created time.Time `json:"created"` |
|
||||||
ContainerConfig struct { |
|
||||||
Cmd []string |
|
||||||
} `json:"container_config,omitempty"` |
|
||||||
Author string `json:"author,omitempty"` |
|
||||||
ThrowAway bool `json:"throwaway,omitempty"` |
|
||||||
} |
|
||||||
|
|
||||||
fsLayerList := make([]FSLayer, len(img.History)) |
|
||||||
history := make([]History, len(img.History)) |
|
||||||
|
|
||||||
parent := "" |
|
||||||
layerCounter := 0 |
|
||||||
for i, h := range img.History[:len(img.History)-1] { |
|
||||||
var blobsum digest.Digest |
|
||||||
if h.EmptyLayer { |
|
||||||
if blobsum, err = mb.emptyTar(ctx); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
} else { |
|
||||||
if len(img.RootFS.DiffIDs) <= layerCounter { |
|
||||||
return nil, errors.New("too many non-empty layers in History section") |
|
||||||
} |
|
||||||
blobsum = mb.descriptors[layerCounter].Digest |
|
||||||
layerCounter++ |
|
||||||
} |
|
||||||
|
|
||||||
v1ID := digest.FromBytes([]byte(blobsum.Hex() + " " + parent)).Hex() |
|
||||||
|
|
||||||
if i == 0 && img.RootFS.BaseLayer != "" { |
|
||||||
// windows-only baselayer setup
|
|
||||||
baseID := sha512.Sum384([]byte(img.RootFS.BaseLayer)) |
|
||||||
parent = fmt.Sprintf("%x", baseID[:32]) |
|
||||||
} |
|
||||||
|
|
||||||
v1Compatibility := v1Compatibility{ |
|
||||||
ID: v1ID, |
|
||||||
Parent: parent, |
|
||||||
Comment: h.Comment, |
|
||||||
Created: h.Created, |
|
||||||
Author: h.Author, |
|
||||||
} |
|
||||||
v1Compatibility.ContainerConfig.Cmd = []string{img.History[i].CreatedBy} |
|
||||||
if h.EmptyLayer { |
|
||||||
v1Compatibility.ThrowAway = true |
|
||||||
} |
|
||||||
jsonBytes, err := json.Marshal(&v1Compatibility) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
reversedIndex := len(img.History) - i - 1 |
|
||||||
history[reversedIndex].V1Compatibility = string(jsonBytes) |
|
||||||
fsLayerList[reversedIndex] = FSLayer{BlobSum: blobsum} |
|
||||||
|
|
||||||
parent = v1ID |
|
||||||
} |
|
||||||
|
|
||||||
latestHistory := img.History[len(img.History)-1] |
|
||||||
|
|
||||||
var blobsum digest.Digest |
|
||||||
if latestHistory.EmptyLayer { |
|
||||||
if blobsum, err = mb.emptyTar(ctx); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
} else { |
|
||||||
if len(img.RootFS.DiffIDs) <= layerCounter { |
|
||||||
return nil, errors.New("too many non-empty layers in History section") |
|
||||||
} |
|
||||||
blobsum = mb.descriptors[layerCounter].Digest |
|
||||||
} |
|
||||||
|
|
||||||
fsLayerList[0] = FSLayer{BlobSum: blobsum} |
|
||||||
dgst := digest.FromBytes([]byte(blobsum.Hex() + " " + parent + " " + string(mb.configJSON))) |
|
||||||
|
|
||||||
// Top-level v1compatibility string should be a modified version of the
|
|
||||||
// image config.
|
|
||||||
transformedConfig, err := MakeV1ConfigFromConfig(mb.configJSON, dgst.Hex(), parent, latestHistory.EmptyLayer) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
history[0].V1Compatibility = string(transformedConfig) |
|
||||||
|
|
||||||
tag := "" |
|
||||||
if tagged, isTagged := mb.ref.(reference.Tagged); isTagged { |
|
||||||
tag = tagged.Tag() |
|
||||||
} |
|
||||||
|
|
||||||
mfst := Manifest{ |
|
||||||
Versioned: manifest.Versioned{ |
|
||||||
SchemaVersion: 1, |
|
||||||
}, |
|
||||||
Name: mb.ref.Name(), |
|
||||||
Tag: tag, |
|
||||||
Architecture: img.Architecture, |
|
||||||
FSLayers: fsLayerList, |
|
||||||
History: history, |
|
||||||
} |
|
||||||
|
|
||||||
return Sign(&mfst, mb.pk) |
|
||||||
} |
|
||||||
|
|
||||||
// emptyTar pushes a compressed empty tar to the blob store if one doesn't
|
|
||||||
// already exist, and returns its blobsum.
|
|
||||||
func (mb *configManifestBuilder) emptyTar(ctx context.Context) (digest.Digest, error) { |
|
||||||
if mb.emptyTarDigest != "" { |
|
||||||
// Already put an empty tar
|
|
||||||
return mb.emptyTarDigest, nil |
|
||||||
} |
|
||||||
|
|
||||||
descriptor, err := mb.bs.Stat(ctx, digestSHA256GzippedEmptyTar) |
|
||||||
switch err { |
|
||||||
case nil: |
|
||||||
mb.emptyTarDigest = descriptor.Digest |
|
||||||
return descriptor.Digest, nil |
|
||||||
case distribution.ErrBlobUnknown: |
|
||||||
// nop
|
|
||||||
default: |
|
||||||
return "", err |
|
||||||
} |
|
||||||
|
|
||||||
// Add gzipped empty tar to the blob store
|
|
||||||
descriptor, err = mb.bs.Put(ctx, "", gzippedEmptyTar) |
|
||||||
if err != nil { |
|
||||||
return "", err |
|
||||||
} |
|
||||||
|
|
||||||
mb.emptyTarDigest = descriptor.Digest |
|
||||||
|
|
||||||
return descriptor.Digest, nil |
|
||||||
} |
|
||||||
|
|
||||||
// AppendReference adds a reference to the current ManifestBuilder
|
|
||||||
func (mb *configManifestBuilder) AppendReference(d distribution.Describable) error { |
|
||||||
descriptor := d.Descriptor() |
|
||||||
|
|
||||||
if err := descriptor.Digest.Validate(); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
mb.descriptors = append(mb.descriptors, descriptor) |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// References returns the current references added to this builder
|
|
||||||
func (mb *configManifestBuilder) References() []distribution.Descriptor { |
|
||||||
return mb.descriptors |
|
||||||
} |
|
||||||
|
|
||||||
// MakeV1ConfigFromConfig creates an legacy V1 image config from image config JSON
|
|
||||||
func MakeV1ConfigFromConfig(configJSON []byte, v1ID, parentV1ID string, throwaway bool) ([]byte, error) { |
|
||||||
// Top-level v1compatibility string should be a modified version of the
|
|
||||||
// image config.
|
|
||||||
var configAsMap map[string]*json.RawMessage |
|
||||||
if err := json.Unmarshal(configJSON, &configAsMap); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
// Delete fields that didn't exist in old manifest
|
|
||||||
delete(configAsMap, "rootfs") |
|
||||||
delete(configAsMap, "history") |
|
||||||
configAsMap["id"] = rawJSON(v1ID) |
|
||||||
if parentV1ID != "" { |
|
||||||
configAsMap["parent"] = rawJSON(parentV1ID) |
|
||||||
} |
|
||||||
if throwaway { |
|
||||||
configAsMap["throwaway"] = rawJSON(true) |
|
||||||
} |
|
||||||
|
|
||||||
return json.Marshal(configAsMap) |
|
||||||
} |
|
||||||
|
|
||||||
func rawJSON(value interface{}) *json.RawMessage { |
|
||||||
jsonval, err := json.Marshal(value) |
|
||||||
if err != nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
return (*json.RawMessage)(&jsonval) |
|
||||||
} |
|
@ -1,184 +0,0 @@ |
|||||||
package schema1 |
|
||||||
|
|
||||||
import ( |
|
||||||
"encoding/json" |
|
||||||
"fmt" |
|
||||||
|
|
||||||
"github.com/docker/distribution" |
|
||||||
"github.com/docker/distribution/manifest" |
|
||||||
"github.com/docker/libtrust" |
|
||||||
"github.com/opencontainers/go-digest" |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
// MediaTypeManifest specifies the mediaType for the current version. Note
|
|
||||||
// that for schema version 1, the the media is optionally "application/json".
|
|
||||||
MediaTypeManifest = "application/vnd.docker.distribution.manifest.v1+json" |
|
||||||
// MediaTypeSignedManifest specifies the mediatype for current SignedManifest version
|
|
||||||
MediaTypeSignedManifest = "application/vnd.docker.distribution.manifest.v1+prettyjws" |
|
||||||
// MediaTypeManifestLayer specifies the media type for manifest layers
|
|
||||||
MediaTypeManifestLayer = "application/vnd.docker.container.image.rootfs.diff+x-gtar" |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
|
||||||
// SchemaVersion provides a pre-initialized version structure for this
|
|
||||||
// packages version of the manifest.
|
|
||||||
SchemaVersion = manifest.Versioned{ |
|
||||||
SchemaVersion: 1, |
|
||||||
} |
|
||||||
) |
|
||||||
|
|
||||||
func init() { |
|
||||||
schema1Func := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) { |
|
||||||
sm := new(SignedManifest) |
|
||||||
err := sm.UnmarshalJSON(b) |
|
||||||
if err != nil { |
|
||||||
return nil, distribution.Descriptor{}, err |
|
||||||
} |
|
||||||
|
|
||||||
desc := distribution.Descriptor{ |
|
||||||
Digest: digest.FromBytes(sm.Canonical), |
|
||||||
Size: int64(len(sm.Canonical)), |
|
||||||
MediaType: MediaTypeSignedManifest, |
|
||||||
} |
|
||||||
return sm, desc, err |
|
||||||
} |
|
||||||
err := distribution.RegisterManifestSchema(MediaTypeSignedManifest, schema1Func) |
|
||||||
if err != nil { |
|
||||||
panic(fmt.Sprintf("Unable to register manifest: %s", err)) |
|
||||||
} |
|
||||||
err = distribution.RegisterManifestSchema("", schema1Func) |
|
||||||
if err != nil { |
|
||||||
panic(fmt.Sprintf("Unable to register manifest: %s", err)) |
|
||||||
} |
|
||||||
err = distribution.RegisterManifestSchema("application/json", schema1Func) |
|
||||||
if err != nil { |
|
||||||
panic(fmt.Sprintf("Unable to register manifest: %s", err)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// FSLayer is a container struct for BlobSums defined in an image manifest
|
|
||||||
type FSLayer struct { |
|
||||||
// BlobSum is the tarsum of the referenced filesystem image layer
|
|
||||||
BlobSum digest.Digest `json:"blobSum"` |
|
||||||
} |
|
||||||
|
|
||||||
// History stores unstructured v1 compatibility information
|
|
||||||
type History struct { |
|
||||||
// V1Compatibility is the raw v1 compatibility information
|
|
||||||
V1Compatibility string `json:"v1Compatibility"` |
|
||||||
} |
|
||||||
|
|
||||||
// Manifest provides the base accessible fields for working with V2 image
|
|
||||||
// format in the registry.
|
|
||||||
type Manifest struct { |
|
||||||
manifest.Versioned |
|
||||||
|
|
||||||
// Name is the name of the image's repository
|
|
||||||
Name string `json:"name"` |
|
||||||
|
|
||||||
// Tag is the tag of the image specified by this manifest
|
|
||||||
Tag string `json:"tag"` |
|
||||||
|
|
||||||
// Architecture is the host architecture on which this image is intended to
|
|
||||||
// run
|
|
||||||
Architecture string `json:"architecture"` |
|
||||||
|
|
||||||
// FSLayers is a list of filesystem layer blobSums contained in this image
|
|
||||||
FSLayers []FSLayer `json:"fsLayers"` |
|
||||||
|
|
||||||
// History is a list of unstructured historical data for v1 compatibility
|
|
||||||
History []History `json:"history"` |
|
||||||
} |
|
||||||
|
|
||||||
// SignedManifest provides an envelope for a signed image manifest, including
|
|
||||||
// the format sensitive raw bytes.
|
|
||||||
type SignedManifest struct { |
|
||||||
Manifest |
|
||||||
|
|
||||||
// Canonical is the canonical byte representation of the ImageManifest,
|
|
||||||
// without any attached signatures. The manifest byte
|
|
||||||
// representation cannot change or it will have to be re-signed.
|
|
||||||
Canonical []byte `json:"-"` |
|
||||||
|
|
||||||
// all contains the byte representation of the Manifest including signatures
|
|
||||||
// and is returned by Payload()
|
|
||||||
all []byte |
|
||||||
} |
|
||||||
|
|
||||||
// UnmarshalJSON populates a new SignedManifest struct from JSON data.
|
|
||||||
func (sm *SignedManifest) UnmarshalJSON(b []byte) error { |
|
||||||
sm.all = make([]byte, len(b), len(b)) |
|
||||||
// store manifest and signatures in all
|
|
||||||
copy(sm.all, b) |
|
||||||
|
|
||||||
jsig, err := libtrust.ParsePrettySignature(b, "signatures") |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// Resolve the payload in the manifest.
|
|
||||||
bytes, err := jsig.Payload() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// sm.Canonical stores the canonical manifest JSON
|
|
||||||
sm.Canonical = make([]byte, len(bytes), len(bytes)) |
|
||||||
copy(sm.Canonical, bytes) |
|
||||||
|
|
||||||
// Unmarshal canonical JSON into Manifest object
|
|
||||||
var manifest Manifest |
|
||||||
if err := json.Unmarshal(sm.Canonical, &manifest); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
sm.Manifest = manifest |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// References returns the descriptors of this manifests references
|
|
||||||
func (sm SignedManifest) References() []distribution.Descriptor { |
|
||||||
dependencies := make([]distribution.Descriptor, len(sm.FSLayers)) |
|
||||||
for i, fsLayer := range sm.FSLayers { |
|
||||||
dependencies[i] = distribution.Descriptor{ |
|
||||||
MediaType: "application/vnd.docker.container.image.rootfs.diff+x-gtar", |
|
||||||
Digest: fsLayer.BlobSum, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return dependencies |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
// MarshalJSON returns the contents of raw. If Raw is nil, marshals the inner
|
|
||||||
// contents. Applications requiring a marshaled signed manifest should simply
|
|
||||||
// use Raw directly, since the the content produced by json.Marshal will be
|
|
||||||
// compacted and will fail signature checks.
|
|
||||||
func (sm *SignedManifest) MarshalJSON() ([]byte, error) { |
|
||||||
if len(sm.all) > 0 { |
|
||||||
return sm.all, nil |
|
||||||
} |
|
||||||
|
|
||||||
// If the raw data is not available, just dump the inner content.
|
|
||||||
return json.Marshal(&sm.Manifest) |
|
||||||
} |
|
||||||
|
|
||||||
// Payload returns the signed content of the signed manifest.
|
|
||||||
func (sm SignedManifest) Payload() (string, []byte, error) { |
|
||||||
return MediaTypeSignedManifest, sm.all, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Signatures returns the signatures as provided by
|
|
||||||
// (*libtrust.JSONSignature).Signatures. The byte slices are opaque jws
|
|
||||||
// signatures.
|
|
||||||
func (sm *SignedManifest) Signatures() ([][]byte, error) { |
|
||||||
jsig, err := libtrust.ParsePrettySignature(sm.all, "signatures") |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
// Resolve the payload in the manifest.
|
|
||||||
return jsig.Signatures() |
|
||||||
} |
|
@ -1,98 +0,0 @@ |
|||||||
package schema1 |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
|
|
||||||
"github.com/docker/distribution" |
|
||||||
"github.com/docker/distribution/manifest" |
|
||||||
"github.com/docker/distribution/reference" |
|
||||||
"github.com/docker/libtrust" |
|
||||||
"github.com/opencontainers/go-digest" |
|
||||||
) |
|
||||||
|
|
||||||
// referenceManifestBuilder is a type for constructing manifests from schema1
|
|
||||||
// dependencies.
|
|
||||||
type referenceManifestBuilder struct { |
|
||||||
Manifest |
|
||||||
pk libtrust.PrivateKey |
|
||||||
} |
|
||||||
|
|
||||||
// NewReferenceManifestBuilder is used to build new manifests for the current
|
|
||||||
// schema version using schema1 dependencies.
|
|
||||||
func NewReferenceManifestBuilder(pk libtrust.PrivateKey, ref reference.Named, architecture string) distribution.ManifestBuilder { |
|
||||||
tag := "" |
|
||||||
if tagged, isTagged := ref.(reference.Tagged); isTagged { |
|
||||||
tag = tagged.Tag() |
|
||||||
} |
|
||||||
|
|
||||||
return &referenceManifestBuilder{ |
|
||||||
Manifest: Manifest{ |
|
||||||
Versioned: manifest.Versioned{ |
|
||||||
SchemaVersion: 1, |
|
||||||
}, |
|
||||||
Name: ref.Name(), |
|
||||||
Tag: tag, |
|
||||||
Architecture: architecture, |
|
||||||
}, |
|
||||||
pk: pk, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (mb *referenceManifestBuilder) Build(ctx context.Context) (distribution.Manifest, error) { |
|
||||||
m := mb.Manifest |
|
||||||
if len(m.FSLayers) == 0 { |
|
||||||
return nil, errors.New("cannot build manifest with zero layers or history") |
|
||||||
} |
|
||||||
|
|
||||||
m.FSLayers = make([]FSLayer, len(mb.Manifest.FSLayers)) |
|
||||||
m.History = make([]History, len(mb.Manifest.History)) |
|
||||||
copy(m.FSLayers, mb.Manifest.FSLayers) |
|
||||||
copy(m.History, mb.Manifest.History) |
|
||||||
|
|
||||||
return Sign(&m, mb.pk) |
|
||||||
} |
|
||||||
|
|
||||||
// AppendReference adds a reference to the current ManifestBuilder
|
|
||||||
func (mb *referenceManifestBuilder) AppendReference(d distribution.Describable) error { |
|
||||||
r, ok := d.(Reference) |
|
||||||
if !ok { |
|
||||||
return fmt.Errorf("Unable to add non-reference type to v1 builder") |
|
||||||
} |
|
||||||
|
|
||||||
// Entries need to be prepended
|
|
||||||
mb.Manifest.FSLayers = append([]FSLayer{{BlobSum: r.Digest}}, mb.Manifest.FSLayers...) |
|
||||||
mb.Manifest.History = append([]History{r.History}, mb.Manifest.History...) |
|
||||||
return nil |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
// References returns the current references added to this builder
|
|
||||||
func (mb *referenceManifestBuilder) References() []distribution.Descriptor { |
|
||||||
refs := make([]distribution.Descriptor, len(mb.Manifest.FSLayers)) |
|
||||||
for i := range mb.Manifest.FSLayers { |
|
||||||
layerDigest := mb.Manifest.FSLayers[i].BlobSum |
|
||||||
history := mb.Manifest.History[i] |
|
||||||
ref := Reference{layerDigest, 0, history} |
|
||||||
refs[i] = ref.Descriptor() |
|
||||||
} |
|
||||||
return refs |
|
||||||
} |
|
||||||
|
|
||||||
// Reference describes a manifest v2, schema version 1 dependency.
|
|
||||||
// An FSLayer associated with a history entry.
|
|
||||||
type Reference struct { |
|
||||||
Digest digest.Digest |
|
||||||
Size int64 // if we know it, set it for the descriptor.
|
|
||||||
History History |
|
||||||
} |
|
||||||
|
|
||||||
// Descriptor describes a reference
|
|
||||||
func (r Reference) Descriptor() distribution.Descriptor { |
|
||||||
return distribution.Descriptor{ |
|
||||||
MediaType: MediaTypeManifestLayer, |
|
||||||
Digest: r.Digest, |
|
||||||
Size: r.Size, |
|
||||||
} |
|
||||||
} |
|
@ -1,68 +0,0 @@ |
|||||||
package schema1 |
|
||||||
|
|
||||||
import ( |
|
||||||
"crypto/x509" |
|
||||||
"encoding/json" |
|
||||||
|
|
||||||
"github.com/docker/libtrust" |
|
||||||
) |
|
||||||
|
|
||||||
// Sign signs the manifest with the provided private key, returning a
|
|
||||||
// SignedManifest. This typically won't be used within the registry, except
|
|
||||||
// for testing.
|
|
||||||
func Sign(m *Manifest, pk libtrust.PrivateKey) (*SignedManifest, error) { |
|
||||||
p, err := json.MarshalIndent(m, "", " ") |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
js, err := libtrust.NewJSONSignature(p) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
if err := js.Sign(pk); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
pretty, err := js.PrettySignature("signatures") |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return &SignedManifest{ |
|
||||||
Manifest: *m, |
|
||||||
all: pretty, |
|
||||||
Canonical: p, |
|
||||||
}, nil |
|
||||||
} |
|
||||||
|
|
||||||
// SignWithChain signs the manifest with the given private key and x509 chain.
|
|
||||||
// The public key of the first element in the chain must be the public key
|
|
||||||
// corresponding with the sign key.
|
|
||||||
func SignWithChain(m *Manifest, key libtrust.PrivateKey, chain []*x509.Certificate) (*SignedManifest, error) { |
|
||||||
p, err := json.MarshalIndent(m, "", " ") |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
js, err := libtrust.NewJSONSignature(p) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
if err := js.SignWithChain(key, chain); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
pretty, err := js.PrettySignature("signatures") |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return &SignedManifest{ |
|
||||||
Manifest: *m, |
|
||||||
all: pretty, |
|
||||||
Canonical: p, |
|
||||||
}, nil |
|
||||||
} |
|
@ -1,32 +0,0 @@ |
|||||||
package schema1 |
|
||||||
|
|
||||||
import ( |
|
||||||
"crypto/x509" |
|
||||||
|
|
||||||
"github.com/docker/libtrust" |
|
||||||
"github.com/sirupsen/logrus" |
|
||||||
) |
|
||||||
|
|
||||||
// Verify verifies the signature of the signed manifest returning the public
|
|
||||||
// keys used during signing.
|
|
||||||
func Verify(sm *SignedManifest) ([]libtrust.PublicKey, error) { |
|
||||||
js, err := libtrust.ParsePrettySignature(sm.all, "signatures") |
|
||||||
if err != nil { |
|
||||||
logrus.WithField("err", err).Debugf("(*SignedManifest).Verify") |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return js.Verify() |
|
||||||
} |
|
||||||
|
|
||||||
// VerifyChains verifies the signature of the signed manifest against the
|
|
||||||
// certificate pool returning the list of verified chains. Signatures without
|
|
||||||
// an x509 chain are not checked.
|
|
||||||
func VerifyChains(sm *SignedManifest, ca *x509.CertPool) ([][]*x509.Certificate, error) { |
|
||||||
js, err := libtrust.ParsePrettySignature(sm.all, "signatures") |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return js.VerifyChains(ca) |
|
||||||
} |
|
@ -1,13 +0,0 @@ |
|||||||
# Contributing to libtrust |
|
||||||
|
|
||||||
Want to hack on libtrust? Awesome! Here are instructions to get you |
|
||||||
started. |
|
||||||
|
|
||||||
libtrust is a part of the [Docker](https://www.docker.com) project, and follows |
|
||||||
the same rules and principles. If you're already familiar with the way |
|
||||||
Docker does things, you'll feel right at home. |
|
||||||
|
|
||||||
Otherwise, go read |
|
||||||
[Docker's contributions guidelines](https://github.com/docker/docker/blob/master/CONTRIBUTING.md). |
|
||||||
|
|
||||||
Happy hacking! |
|
@ -1,191 +0,0 @@ |
|||||||
|
|
||||||
Apache License |
|
||||||
Version 2.0, January 2004 |
|
||||||
http://www.apache.org/licenses/ |
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
|
||||||
|
|
||||||
1. Definitions. |
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction, |
|
||||||
and distribution as defined by Sections 1 through 9 of this document. |
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by |
|
||||||
the copyright owner that is granting the License. |
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all |
|
||||||
other entities that control, are controlled by, or are under common |
|
||||||
control with that entity. For the purposes of this definition, |
|
||||||
"control" means (i) the power, direct or indirect, to cause the |
|
||||||
direction or management of such entity, whether by contract or |
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the |
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity. |
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity |
|
||||||
exercising permissions granted by this License. |
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications, |
|
||||||
including but not limited to software source code, documentation |
|
||||||
source, and configuration files. |
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical |
|
||||||
transformation or translation of a Source form, including but |
|
||||||
not limited to compiled object code, generated documentation, |
|
||||||
and conversions to other media types. |
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or |
|
||||||
Object form, made available under the License, as indicated by a |
|
||||||
copyright notice that is included in or attached to the work |
|
||||||
(an example is provided in the Appendix below). |
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object |
|
||||||
form, that is based on (or derived from) the Work and for which the |
|
||||||
editorial revisions, annotations, elaborations, or other modifications |
|
||||||
represent, as a whole, an original work of authorship. For the purposes |
|
||||||
of this License, Derivative Works shall not include works that remain |
|
||||||
separable from, or merely link (or bind by name) to the interfaces of, |
|
||||||
the Work and Derivative Works thereof. |
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including |
|
||||||
the original version of the Work and any modifications or additions |
|
||||||
to that Work or Derivative Works thereof, that is intentionally |
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner |
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of |
|
||||||
the copyright owner. For the purposes of this definition, "submitted" |
|
||||||
means any form of electronic, verbal, or written communication sent |
|
||||||
to the Licensor or its representatives, including but not limited to |
|
||||||
communication on electronic mailing lists, source code control systems, |
|
||||||
and issue tracking systems that are managed by, or on behalf of, the |
|
||||||
Licensor for the purpose of discussing and improving the Work, but |
|
||||||
excluding communication that is conspicuously marked or otherwise |
|
||||||
designated in writing by the copyright owner as "Not a Contribution." |
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity |
|
||||||
on behalf of whom a Contribution has been received by Licensor and |
|
||||||
subsequently incorporated within the Work. |
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of |
|
||||||
this License, each Contributor hereby grants to You a perpetual, |
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
|
||||||
copyright license to reproduce, prepare Derivative Works of, |
|
||||||
publicly display, publicly perform, sublicense, and distribute the |
|
||||||
Work and such Derivative Works in Source or Object form. |
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of |
|
||||||
this License, each Contributor hereby grants to You a perpetual, |
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
|
||||||
(except as stated in this section) patent license to make, have made, |
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work, |
|
||||||
where such license applies only to those patent claims licensable |
|
||||||
by such Contributor that are necessarily infringed by their |
|
||||||
Contribution(s) alone or by combination of their Contribution(s) |
|
||||||
with the Work to which such Contribution(s) was submitted. If You |
|
||||||
institute patent litigation against any entity (including a |
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work |
|
||||||
or a Contribution incorporated within the Work constitutes direct |
|
||||||
or contributory patent infringement, then any patent licenses |
|
||||||
granted to You under this License for that Work shall terminate |
|
||||||
as of the date such litigation is filed. |
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the |
|
||||||
Work or Derivative Works thereof in any medium, with or without |
|
||||||
modifications, and in Source or Object form, provided that You |
|
||||||
meet the following conditions: |
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or |
|
||||||
Derivative Works a copy of this License; and |
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices |
|
||||||
stating that You changed the files; and |
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works |
|
||||||
that You distribute, all copyright, patent, trademark, and |
|
||||||
attribution notices from the Source form of the Work, |
|
||||||
excluding those notices that do not pertain to any part of |
|
||||||
the Derivative Works; and |
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its |
|
||||||
distribution, then any Derivative Works that You distribute must |
|
||||||
include a readable copy of the attribution notices contained |
|
||||||
within such NOTICE file, excluding those notices that do not |
|
||||||
pertain to any part of the Derivative Works, in at least one |
|
||||||
of the following places: within a NOTICE text file distributed |
|
||||||
as part of the Derivative Works; within the Source form or |
|
||||||
documentation, if provided along with the Derivative Works; or, |
|
||||||
within a display generated by the Derivative Works, if and |
|
||||||
wherever such third-party notices normally appear. The contents |
|
||||||
of the NOTICE file are for informational purposes only and |
|
||||||
do not modify the License. You may add Your own attribution |
|
||||||
notices within Derivative Works that You distribute, alongside |
|
||||||
or as an addendum to the NOTICE text from the Work, provided |
|
||||||
that such additional attribution notices cannot be construed |
|
||||||
as modifying the License. |
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and |
|
||||||
may provide additional or different license terms and conditions |
|
||||||
for use, reproduction, or distribution of Your modifications, or |
|
||||||
for any such Derivative Works as a whole, provided Your use, |
|
||||||
reproduction, and distribution of the Work otherwise complies with |
|
||||||
the conditions stated in this License. |
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise, |
|
||||||
any Contribution intentionally submitted for inclusion in the Work |
|
||||||
by You to the Licensor shall be under the terms and conditions of |
|
||||||
this License, without any additional terms or conditions. |
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify |
|
||||||
the terms of any separate license agreement you may have executed |
|
||||||
with Licensor regarding such Contributions. |
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade |
|
||||||
names, trademarks, service marks, or product names of the Licensor, |
|
||||||
except as required for reasonable and customary use in describing the |
|
||||||
origin of the Work and reproducing the content of the NOTICE file. |
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or |
|
||||||
agreed to in writing, Licensor provides the Work (and each |
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS, |
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
|
||||||
implied, including, without limitation, any warranties or conditions |
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the |
|
||||||
appropriateness of using or redistributing the Work and assume any |
|
||||||
risks associated with Your exercise of permissions under this License. |
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory, |
|
||||||
whether in tort (including negligence), contract, or otherwise, |
|
||||||
unless required by applicable law (such as deliberate and grossly |
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be |
|
||||||
liable to You for damages, including any direct, indirect, special, |
|
||||||
incidental, or consequential damages of any character arising as a |
|
||||||
result of this License or out of the use or inability to use the |
|
||||||
Work (including but not limited to damages for loss of goodwill, |
|
||||||
work stoppage, computer failure or malfunction, or any and all |
|
||||||
other commercial damages or losses), even if such Contributor |
|
||||||
has been advised of the possibility of such damages. |
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing |
|
||||||
the Work or Derivative Works thereof, You may choose to offer, |
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity, |
|
||||||
or other liability obligations and/or rights consistent with this |
|
||||||
License. However, in accepting such obligations, You may act only |
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf |
|
||||||
of any other Contributor, and only if You agree to indemnify, |
|
||||||
defend, and hold each Contributor harmless for any liability |
|
||||||
incurred by, or claims asserted against, such Contributor by reason |
|
||||||
of your accepting any such warranty or additional liability. |
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS |
|
||||||
|
|
||||||
Copyright 2014 Docker, Inc. |
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License"); |
|
||||||
you may not use this file except in compliance with the License. |
|
||||||
You may obtain a copy of the License at |
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0 |
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software |
|
||||||
distributed under the License is distributed on an "AS IS" BASIS, |
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
||||||
See the License for the specific language governing permissions and |
|
||||||
limitations under the License. |
|
@ -1,3 +0,0 @@ |
|||||||
Solomon Hykes <solomon@docker.com> |
|
||||||
Josh Hawn <josh@docker.com> (github: jlhawn) |
|
||||||
Derek McGowan <derek@docker.com> (github: dmcgowan) |
|
@ -1,22 +0,0 @@ |
|||||||
# libtrust |
|
||||||
|
|
||||||
> **WARNING** this library is no longer actively developed, and will be integrated |
|
||||||
> in the [docker/distribution][https://www.github.com/docker/distribution] |
|
||||||
> repository in future. |
|
||||||
|
|
||||||
Libtrust is library for managing authentication and authorization using public key cryptography. |
|
||||||
|
|
||||||
Authentication is handled using the identity attached to the public key. |
|
||||||
Libtrust provides multiple methods to prove possession of the private key associated with an identity. |
|
||||||
- TLS x509 certificates |
|
||||||
- Signature verification |
|
||||||
- Key Challenge |
|
||||||
|
|
||||||
Authorization and access control is managed through a distributed trust graph. |
|
||||||
Trust servers are used as the authorities of the trust graph and allow caching portions of the graph for faster access. |
|
||||||
|
|
||||||
## Copyright and license |
|
||||||
|
|
||||||
Code and documentation copyright 2014 Docker, inc. Code released under the Apache 2.0 license. |
|
||||||
Docs released under Creative commons. |
|
||||||
|
|
@ -1,175 +0,0 @@ |
|||||||
package libtrust |
|
||||||
|
|
||||||
import ( |
|
||||||
"crypto/rand" |
|
||||||
"crypto/x509" |
|
||||||
"crypto/x509/pkix" |
|
||||||
"encoding/pem" |
|
||||||
"fmt" |
|
||||||
"io/ioutil" |
|
||||||
"math/big" |
|
||||||
"net" |
|
||||||
"time" |
|
||||||
) |
|
||||||
|
|
||||||
type certTemplateInfo struct { |
|
||||||
commonName string |
|
||||||
domains []string |
|
||||||
ipAddresses []net.IP |
|
||||||
isCA bool |
|
||||||
clientAuth bool |
|
||||||
serverAuth bool |
|
||||||
} |
|
||||||
|
|
||||||
func generateCertTemplate(info *certTemplateInfo) *x509.Certificate { |
|
||||||
// Generate a certificate template which is valid from the past week to
|
|
||||||
// 10 years from now. The usage of the certificate depends on the
|
|
||||||
// specified fields in the given certTempInfo object.
|
|
||||||
var ( |
|
||||||
keyUsage x509.KeyUsage |
|
||||||
extKeyUsage []x509.ExtKeyUsage |
|
||||||
) |
|
||||||
|
|
||||||
if info.isCA { |
|
||||||
keyUsage = x509.KeyUsageCertSign |
|
||||||
} |
|
||||||
|
|
||||||
if info.clientAuth { |
|
||||||
extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageClientAuth) |
|
||||||
} |
|
||||||
|
|
||||||
if info.serverAuth { |
|
||||||
extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageServerAuth) |
|
||||||
} |
|
||||||
|
|
||||||
return &x509.Certificate{ |
|
||||||
SerialNumber: big.NewInt(0), |
|
||||||
Subject: pkix.Name{ |
|
||||||
CommonName: info.commonName, |
|
||||||
}, |
|
||||||
NotBefore: time.Now().Add(-time.Hour * 24 * 7), |
|
||||||
NotAfter: time.Now().Add(time.Hour * 24 * 365 * 10), |
|
||||||
DNSNames: info.domains, |
|
||||||
IPAddresses: info.ipAddresses, |
|
||||||
IsCA: info.isCA, |
|
||||||
KeyUsage: keyUsage, |
|
||||||
ExtKeyUsage: extKeyUsage, |
|
||||||
BasicConstraintsValid: info.isCA, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func generateCert(pub PublicKey, priv PrivateKey, subInfo, issInfo *certTemplateInfo) (cert *x509.Certificate, err error) { |
|
||||||
pubCertTemplate := generateCertTemplate(subInfo) |
|
||||||
privCertTemplate := generateCertTemplate(issInfo) |
|
||||||
|
|
||||||
certDER, err := x509.CreateCertificate( |
|
||||||
rand.Reader, pubCertTemplate, privCertTemplate, |
|
||||||
pub.CryptoPublicKey(), priv.CryptoPrivateKey(), |
|
||||||
) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("failed to create certificate: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
cert, err = x509.ParseCertificate(certDER) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("failed to parse certificate: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// GenerateSelfSignedServerCert creates a self-signed certificate for the
|
|
||||||
// given key which is to be used for TLS servers with the given domains and
|
|
||||||
// IP addresses.
|
|
||||||
func GenerateSelfSignedServerCert(key PrivateKey, domains []string, ipAddresses []net.IP) (*x509.Certificate, error) { |
|
||||||
info := &certTemplateInfo{ |
|
||||||
commonName: key.KeyID(), |
|
||||||
domains: domains, |
|
||||||
ipAddresses: ipAddresses, |
|
||||||
serverAuth: true, |
|
||||||
} |
|
||||||
|
|
||||||
return generateCert(key.PublicKey(), key, info, info) |
|
||||||
} |
|
||||||
|
|
||||||
// GenerateSelfSignedClientCert creates a self-signed certificate for the
|
|
||||||
// given key which is to be used for TLS clients.
|
|
||||||
func GenerateSelfSignedClientCert(key PrivateKey) (*x509.Certificate, error) { |
|
||||||
info := &certTemplateInfo{ |
|
||||||
commonName: key.KeyID(), |
|
||||||
clientAuth: true, |
|
||||||
} |
|
||||||
|
|
||||||
return generateCert(key.PublicKey(), key, info, info) |
|
||||||
} |
|
||||||
|
|
||||||
// GenerateCACert creates a certificate which can be used as a trusted
|
|
||||||
// certificate authority.
|
|
||||||
func GenerateCACert(signer PrivateKey, trustedKey PublicKey) (*x509.Certificate, error) { |
|
||||||
subjectInfo := &certTemplateInfo{ |
|
||||||
commonName: trustedKey.KeyID(), |
|
||||||
isCA: true, |
|
||||||
} |
|
||||||
issuerInfo := &certTemplateInfo{ |
|
||||||
commonName: signer.KeyID(), |
|
||||||
} |
|
||||||
|
|
||||||
return generateCert(trustedKey, signer, subjectInfo, issuerInfo) |
|
||||||
} |
|
||||||
|
|
||||||
// GenerateCACertPool creates a certificate authority pool to be used for a
|
|
||||||
// TLS configuration. Any self-signed certificates issued by the specified
|
|
||||||
// trusted keys will be verified during a TLS handshake
|
|
||||||
func GenerateCACertPool(signer PrivateKey, trustedKeys []PublicKey) (*x509.CertPool, error) { |
|
||||||
certPool := x509.NewCertPool() |
|
||||||
|
|
||||||
for _, trustedKey := range trustedKeys { |
|
||||||
cert, err := GenerateCACert(signer, trustedKey) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("failed to generate CA certificate: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
certPool.AddCert(cert) |
|
||||||
} |
|
||||||
|
|
||||||
return certPool, nil |
|
||||||
} |
|
||||||
|
|
||||||
// LoadCertificateBundle loads certificates from the given file. The file should be pem encoded
|
|
||||||
// containing one or more certificates. The expected pem type is "CERTIFICATE".
|
|
||||||
func LoadCertificateBundle(filename string) ([]*x509.Certificate, error) { |
|
||||||
b, err := ioutil.ReadFile(filename) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
certificates := []*x509.Certificate{} |
|
||||||
var block *pem.Block |
|
||||||
block, b = pem.Decode(b) |
|
||||||
for ; block != nil; block, b = pem.Decode(b) { |
|
||||||
if block.Type == "CERTIFICATE" { |
|
||||||
cert, err := x509.ParseCertificate(block.Bytes) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
certificates = append(certificates, cert) |
|
||||||
} else { |
|
||||||
return nil, fmt.Errorf("invalid pem block type: %s", block.Type) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return certificates, nil |
|
||||||
} |
|
||||||
|
|
||||||
// LoadCertificatePool loads a CA pool from the given file. The file should be pem encoded
|
|
||||||
// containing one or more certificates. The expected pem type is "CERTIFICATE".
|
|
||||||
func LoadCertificatePool(filename string) (*x509.CertPool, error) { |
|
||||||
certs, err := LoadCertificateBundle(filename) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
pool := x509.NewCertPool() |
|
||||||
for _, cert := range certs { |
|
||||||
pool.AddCert(cert) |
|
||||||
} |
|
||||||
return pool, nil |
|
||||||
} |
|
@ -1,9 +0,0 @@ |
|||||||
/* |
|
||||||
Package libtrust provides an interface for managing authentication and |
|
||||||
authorization using public key cryptography. Authentication is handled |
|
||||||
using the identity attached to the public key and verified through TLS |
|
||||||
x509 certificates, a key challenge, or signature. Authorization and |
|
||||||
access control is managed through a trust graph distributed between |
|
||||||
both remote trust servers and locally cached and managed data. |
|
||||||
*/ |
|
||||||
package libtrust |
|
@ -1,428 +0,0 @@ |
|||||||
package libtrust |
|
||||||
|
|
||||||
import ( |
|
||||||
"crypto" |
|
||||||
"crypto/ecdsa" |
|
||||||
"crypto/elliptic" |
|
||||||
"crypto/rand" |
|
||||||
"crypto/x509" |
|
||||||
"encoding/json" |
|
||||||
"encoding/pem" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"io" |
|
||||||
"math/big" |
|
||||||
) |
|
||||||
|
|
||||||
/* |
|
||||||
* EC DSA PUBLIC KEY |
|
||||||
*/ |
|
||||||
|
|
||||||
// ecPublicKey implements a libtrust.PublicKey using elliptic curve digital
|
|
||||||
// signature algorithms.
|
|
||||||
type ecPublicKey struct { |
|
||||||
*ecdsa.PublicKey |
|
||||||
curveName string |
|
||||||
signatureAlgorithm *signatureAlgorithm |
|
||||||
extended map[string]interface{} |
|
||||||
} |
|
||||||
|
|
||||||
func fromECPublicKey(cryptoPublicKey *ecdsa.PublicKey) (*ecPublicKey, error) { |
|
||||||
curve := cryptoPublicKey.Curve |
|
||||||
|
|
||||||
switch { |
|
||||||
case curve == elliptic.P256(): |
|
||||||
return &ecPublicKey{cryptoPublicKey, "P-256", es256, map[string]interface{}{}}, nil |
|
||||||
case curve == elliptic.P384(): |
|
||||||
return &ecPublicKey{cryptoPublicKey, "P-384", es384, map[string]interface{}{}}, nil |
|
||||||
case curve == elliptic.P521(): |
|
||||||
return &ecPublicKey{cryptoPublicKey, "P-521", es512, map[string]interface{}{}}, nil |
|
||||||
default: |
|
||||||
return nil, errors.New("unsupported elliptic curve") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// KeyType returns the key type for elliptic curve keys, i.e., "EC".
|
|
||||||
func (k *ecPublicKey) KeyType() string { |
|
||||||
return "EC" |
|
||||||
} |
|
||||||
|
|
||||||
// CurveName returns the elliptic curve identifier.
|
|
||||||
// Possible values are "P-256", "P-384", and "P-521".
|
|
||||||
func (k *ecPublicKey) CurveName() string { |
|
||||||
return k.curveName |
|
||||||
} |
|
||||||
|
|
||||||
// KeyID returns a distinct identifier which is unique to this Public Key.
|
|
||||||
func (k *ecPublicKey) KeyID() string { |
|
||||||
return keyIDFromCryptoKey(k) |
|
||||||
} |
|
||||||
|
|
||||||
func (k *ecPublicKey) String() string { |
|
||||||
return fmt.Sprintf("EC Public Key <%s>", k.KeyID()) |
|
||||||
} |
|
||||||
|
|
||||||
// Verify verifyies the signature of the data in the io.Reader using this
|
|
||||||
// PublicKey. The alg parameter should identify the digital signature
|
|
||||||
// algorithm which was used to produce the signature and should be supported
|
|
||||||
// by this public key. Returns a nil error if the signature is valid.
|
|
||||||
func (k *ecPublicKey) Verify(data io.Reader, alg string, signature []byte) error { |
|
||||||
// For EC keys there is only one supported signature algorithm depending
|
|
||||||
// on the curve parameters.
|
|
||||||
if k.signatureAlgorithm.HeaderParam() != alg { |
|
||||||
return fmt.Errorf("unable to verify signature: EC Public Key with curve %q does not support signature algorithm %q", k.curveName, alg) |
|
||||||
} |
|
||||||
|
|
||||||
// signature is the concatenation of (r, s), base64Url encoded.
|
|
||||||
sigLength := len(signature) |
|
||||||
expectedOctetLength := 2 * ((k.Params().BitSize + 7) >> 3) |
|
||||||
if sigLength != expectedOctetLength { |
|
||||||
return fmt.Errorf("signature length is %d octets long, should be %d", sigLength, expectedOctetLength) |
|
||||||
} |
|
||||||
|
|
||||||
rBytes, sBytes := signature[:sigLength/2], signature[sigLength/2:] |
|
||||||
r := new(big.Int).SetBytes(rBytes) |
|
||||||
s := new(big.Int).SetBytes(sBytes) |
|
||||||
|
|
||||||
hasher := k.signatureAlgorithm.HashID().New() |
|
||||||
_, err := io.Copy(hasher, data) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("error reading data to sign: %s", err) |
|
||||||
} |
|
||||||
hash := hasher.Sum(nil) |
|
||||||
|
|
||||||
if !ecdsa.Verify(k.PublicKey, hash, r, s) { |
|
||||||
return errors.New("invalid signature") |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// CryptoPublicKey returns the internal object which can be used as a
|
|
||||||
// crypto.PublicKey for use with other standard library operations. The type
|
|
||||||
// is either *rsa.PublicKey or *ecdsa.PublicKey
|
|
||||||
func (k *ecPublicKey) CryptoPublicKey() crypto.PublicKey { |
|
||||||
return k.PublicKey |
|
||||||
} |
|
||||||
|
|
||||||
func (k *ecPublicKey) toMap() map[string]interface{} { |
|
||||||
jwk := make(map[string]interface{}) |
|
||||||
for k, v := range k.extended { |
|
||||||
jwk[k] = v |
|
||||||
} |
|
||||||
jwk["kty"] = k.KeyType() |
|
||||||
jwk["kid"] = k.KeyID() |
|
||||||
jwk["crv"] = k.CurveName() |
|
||||||
|
|
||||||
xBytes := k.X.Bytes() |
|
||||||
yBytes := k.Y.Bytes() |
|
||||||
octetLength := (k.Params().BitSize + 7) >> 3 |
|
||||||
// MUST include leading zeros in the output so that x, y are each
|
|
||||||
// *octetLength* bytes long.
|
|
||||||
xBuf := make([]byte, octetLength-len(xBytes), octetLength) |
|
||||||
yBuf := make([]byte, octetLength-len(yBytes), octetLength) |
|
||||||
xBuf = append(xBuf, xBytes...) |
|
||||||
yBuf = append(yBuf, yBytes...) |
|
||||||
|
|
||||||
jwk["x"] = joseBase64UrlEncode(xBuf) |
|
||||||
jwk["y"] = joseBase64UrlEncode(yBuf) |
|
||||||
|
|
||||||
return jwk |
|
||||||
} |
|
||||||
|
|
||||||
// MarshalJSON serializes this Public Key using the JWK JSON serialization format for
|
|
||||||
// elliptic curve keys.
|
|
||||||
func (k *ecPublicKey) MarshalJSON() (data []byte, err error) { |
|
||||||
return json.Marshal(k.toMap()) |
|
||||||
} |
|
||||||
|
|
||||||
// PEMBlock serializes this Public Key to DER-encoded PKIX format.
|
|
||||||
func (k *ecPublicKey) PEMBlock() (*pem.Block, error) { |
|
||||||
derBytes, err := x509.MarshalPKIXPublicKey(k.PublicKey) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("unable to serialize EC PublicKey to DER-encoded PKIX format: %s", err) |
|
||||||
} |
|
||||||
k.extended["kid"] = k.KeyID() // For display purposes.
|
|
||||||
return createPemBlock("PUBLIC KEY", derBytes, k.extended) |
|
||||||
} |
|
||||||
|
|
||||||
func (k *ecPublicKey) AddExtendedField(field string, value interface{}) { |
|
||||||
k.extended[field] = value |
|
||||||
} |
|
||||||
|
|
||||||
func (k *ecPublicKey) GetExtendedField(field string) interface{} { |
|
||||||
v, ok := k.extended[field] |
|
||||||
if !ok { |
|
||||||
return nil |
|
||||||
} |
|
||||||
return v |
|
||||||
} |
|
||||||
|
|
||||||
func ecPublicKeyFromMap(jwk map[string]interface{}) (*ecPublicKey, error) { |
|
||||||
// JWK key type (kty) has already been determined to be "EC".
|
|
||||||
// Need to extract 'crv', 'x', 'y', and 'kid' and check for
|
|
||||||
// consistency.
|
|
||||||
|
|
||||||
// Get the curve identifier value.
|
|
||||||
crv, err := stringFromMap(jwk, "crv") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK EC Public Key curve identifier: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
var ( |
|
||||||
curve elliptic.Curve |
|
||||||
sigAlg *signatureAlgorithm |
|
||||||
) |
|
||||||
|
|
||||||
switch { |
|
||||||
case crv == "P-256": |
|
||||||
curve = elliptic.P256() |
|
||||||
sigAlg = es256 |
|
||||||
case crv == "P-384": |
|
||||||
curve = elliptic.P384() |
|
||||||
sigAlg = es384 |
|
||||||
case crv == "P-521": |
|
||||||
curve = elliptic.P521() |
|
||||||
sigAlg = es512 |
|
||||||
default: |
|
||||||
return nil, fmt.Errorf("JWK EC Public Key curve identifier not supported: %q\n", crv) |
|
||||||
} |
|
||||||
|
|
||||||
// Get the X and Y coordinates for the public key point.
|
|
||||||
xB64Url, err := stringFromMap(jwk, "x") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK EC Public Key x-coordinate: %s", err) |
|
||||||
} |
|
||||||
x, err := parseECCoordinate(xB64Url, curve) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK EC Public Key x-coordinate: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
yB64Url, err := stringFromMap(jwk, "y") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK EC Public Key y-coordinate: %s", err) |
|
||||||
} |
|
||||||
y, err := parseECCoordinate(yB64Url, curve) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK EC Public Key y-coordinate: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
key := &ecPublicKey{ |
|
||||||
PublicKey: &ecdsa.PublicKey{Curve: curve, X: x, Y: y}, |
|
||||||
curveName: crv, signatureAlgorithm: sigAlg, |
|
||||||
} |
|
||||||
|
|
||||||
// Key ID is optional too, but if it exists, it should match the key.
|
|
||||||
_, ok := jwk["kid"] |
|
||||||
if ok { |
|
||||||
kid, err := stringFromMap(jwk, "kid") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK EC Public Key ID: %s", err) |
|
||||||
} |
|
||||||
if kid != key.KeyID() { |
|
||||||
return nil, fmt.Errorf("JWK EC Public Key ID does not match: %s", kid) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
key.extended = jwk |
|
||||||
|
|
||||||
return key, nil |
|
||||||
} |
|
||||||
|
|
||||||
/* |
|
||||||
* EC DSA PRIVATE KEY |
|
||||||
*/ |
|
||||||
|
|
||||||
// ecPrivateKey implements a JWK Private Key using elliptic curve digital signature
|
|
||||||
// algorithms.
|
|
||||||
type ecPrivateKey struct { |
|
||||||
ecPublicKey |
|
||||||
*ecdsa.PrivateKey |
|
||||||
} |
|
||||||
|
|
||||||
func fromECPrivateKey(cryptoPrivateKey *ecdsa.PrivateKey) (*ecPrivateKey, error) { |
|
||||||
publicKey, err := fromECPublicKey(&cryptoPrivateKey.PublicKey) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return &ecPrivateKey{*publicKey, cryptoPrivateKey}, nil |
|
||||||
} |
|
||||||
|
|
||||||
// PublicKey returns the Public Key data associated with this Private Key.
|
|
||||||
func (k *ecPrivateKey) PublicKey() PublicKey { |
|
||||||
return &k.ecPublicKey |
|
||||||
} |
|
||||||
|
|
||||||
func (k *ecPrivateKey) String() string { |
|
||||||
return fmt.Sprintf("EC Private Key <%s>", k.KeyID()) |
|
||||||
} |
|
||||||
|
|
||||||
// Sign signs the data read from the io.Reader using a signature algorithm supported
|
|
||||||
// by the elliptic curve private key. If the specified hashing algorithm is
|
|
||||||
// supported by this key, that hash function is used to generate the signature
|
|
||||||
// otherwise the the default hashing algorithm for this key is used. Returns
|
|
||||||
// the signature and the name of the JWK signature algorithm used, e.g.,
|
|
||||||
// "ES256", "ES384", "ES512".
|
|
||||||
func (k *ecPrivateKey) Sign(data io.Reader, hashID crypto.Hash) (signature []byte, alg string, err error) { |
|
||||||
// Generate a signature of the data using the internal alg.
|
|
||||||
// The given hashId is only a suggestion, and since EC keys only support
|
|
||||||
// on signature/hash algorithm given the curve name, we disregard it for
|
|
||||||
// the elliptic curve JWK signature implementation.
|
|
||||||
hasher := k.signatureAlgorithm.HashID().New() |
|
||||||
_, err = io.Copy(hasher, data) |
|
||||||
if err != nil { |
|
||||||
return nil, "", fmt.Errorf("error reading data to sign: %s", err) |
|
||||||
} |
|
||||||
hash := hasher.Sum(nil) |
|
||||||
|
|
||||||
r, s, err := ecdsa.Sign(rand.Reader, k.PrivateKey, hash) |
|
||||||
if err != nil { |
|
||||||
return nil, "", fmt.Errorf("error producing signature: %s", err) |
|
||||||
} |
|
||||||
rBytes, sBytes := r.Bytes(), s.Bytes() |
|
||||||
octetLength := (k.ecPublicKey.Params().BitSize + 7) >> 3 |
|
||||||
// MUST include leading zeros in the output
|
|
||||||
rBuf := make([]byte, octetLength-len(rBytes), octetLength) |
|
||||||
sBuf := make([]byte, octetLength-len(sBytes), octetLength) |
|
||||||
|
|
||||||
rBuf = append(rBuf, rBytes...) |
|
||||||
sBuf = append(sBuf, sBytes...) |
|
||||||
|
|
||||||
signature = append(rBuf, sBuf...) |
|
||||||
alg = k.signatureAlgorithm.HeaderParam() |
|
||||||
|
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// CryptoPrivateKey returns the internal object which can be used as a
|
|
||||||
// crypto.PublicKey for use with other standard library operations. The type
|
|
||||||
// is either *rsa.PublicKey or *ecdsa.PublicKey
|
|
||||||
func (k *ecPrivateKey) CryptoPrivateKey() crypto.PrivateKey { |
|
||||||
return k.PrivateKey |
|
||||||
} |
|
||||||
|
|
||||||
func (k *ecPrivateKey) toMap() map[string]interface{} { |
|
||||||
jwk := k.ecPublicKey.toMap() |
|
||||||
|
|
||||||
dBytes := k.D.Bytes() |
|
||||||
// The length of this octet string MUST be ceiling(log-base-2(n)/8)
|
|
||||||
// octets (where n is the order of the curve). This is because the private
|
|
||||||
// key d must be in the interval [1, n-1] so the bitlength of d should be
|
|
||||||
// no larger than the bitlength of n-1. The easiest way to find the octet
|
|
||||||
// length is to take bitlength(n-1), add 7 to force a carry, and shift this
|
|
||||||
// bit sequence right by 3, which is essentially dividing by 8 and adding
|
|
||||||
// 1 if there is any remainder. Thus, the private key value d should be
|
|
||||||
// output to (bitlength(n-1)+7)>>3 octets.
|
|
||||||
n := k.ecPublicKey.Params().N |
|
||||||
octetLength := (new(big.Int).Sub(n, big.NewInt(1)).BitLen() + 7) >> 3 |
|
||||||
// Create a buffer with the necessary zero-padding.
|
|
||||||
dBuf := make([]byte, octetLength-len(dBytes), octetLength) |
|
||||||
dBuf = append(dBuf, dBytes...) |
|
||||||
|
|
||||||
jwk["d"] = joseBase64UrlEncode(dBuf) |
|
||||||
|
|
||||||
return jwk |
|
||||||
} |
|
||||||
|
|
||||||
// MarshalJSON serializes this Private Key using the JWK JSON serialization format for
|
|
||||||
// elliptic curve keys.
|
|
||||||
func (k *ecPrivateKey) MarshalJSON() (data []byte, err error) { |
|
||||||
return json.Marshal(k.toMap()) |
|
||||||
} |
|
||||||
|
|
||||||
// PEMBlock serializes this Private Key to DER-encoded PKIX format.
|
|
||||||
func (k *ecPrivateKey) PEMBlock() (*pem.Block, error) { |
|
||||||
derBytes, err := x509.MarshalECPrivateKey(k.PrivateKey) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("unable to serialize EC PrivateKey to DER-encoded PKIX format: %s", err) |
|
||||||
} |
|
||||||
k.extended["keyID"] = k.KeyID() // For display purposes.
|
|
||||||
return createPemBlock("EC PRIVATE KEY", derBytes, k.extended) |
|
||||||
} |
|
||||||
|
|
||||||
func ecPrivateKeyFromMap(jwk map[string]interface{}) (*ecPrivateKey, error) { |
|
||||||
dB64Url, err := stringFromMap(jwk, "d") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK EC Private Key: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
// JWK key type (kty) has already been determined to be "EC".
|
|
||||||
// Need to extract the public key information, then extract the private
|
|
||||||
// key value 'd'.
|
|
||||||
publicKey, err := ecPublicKeyFromMap(jwk) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
d, err := parseECPrivateParam(dB64Url, publicKey.Curve) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK EC Private Key d-param: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
key := &ecPrivateKey{ |
|
||||||
ecPublicKey: *publicKey, |
|
||||||
PrivateKey: &ecdsa.PrivateKey{ |
|
||||||
PublicKey: *publicKey.PublicKey, |
|
||||||
D: d, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
return key, nil |
|
||||||
} |
|
||||||
|
|
||||||
/* |
|
||||||
* Key Generation Functions. |
|
||||||
*/ |
|
||||||
|
|
||||||
func generateECPrivateKey(curve elliptic.Curve) (k *ecPrivateKey, err error) { |
|
||||||
k = new(ecPrivateKey) |
|
||||||
k.PrivateKey, err = ecdsa.GenerateKey(curve, rand.Reader) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
k.ecPublicKey.PublicKey = &k.PrivateKey.PublicKey |
|
||||||
k.extended = make(map[string]interface{}) |
|
||||||
|
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// GenerateECP256PrivateKey generates a key pair using elliptic curve P-256.
|
|
||||||
func GenerateECP256PrivateKey() (PrivateKey, error) { |
|
||||||
k, err := generateECPrivateKey(elliptic.P256()) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("error generating EC P-256 key: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
k.curveName = "P-256" |
|
||||||
k.signatureAlgorithm = es256 |
|
||||||
|
|
||||||
return k, nil |
|
||||||
} |
|
||||||
|
|
||||||
// GenerateECP384PrivateKey generates a key pair using elliptic curve P-384.
|
|
||||||
func GenerateECP384PrivateKey() (PrivateKey, error) { |
|
||||||
k, err := generateECPrivateKey(elliptic.P384()) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("error generating EC P-384 key: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
k.curveName = "P-384" |
|
||||||
k.signatureAlgorithm = es384 |
|
||||||
|
|
||||||
return k, nil |
|
||||||
} |
|
||||||
|
|
||||||
// GenerateECP521PrivateKey generates aß key pair using elliptic curve P-521.
|
|
||||||
func GenerateECP521PrivateKey() (PrivateKey, error) { |
|
||||||
k, err := generateECPrivateKey(elliptic.P521()) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("error generating EC P-521 key: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
k.curveName = "P-521" |
|
||||||
k.signatureAlgorithm = es512 |
|
||||||
|
|
||||||
return k, nil |
|
||||||
} |
|
@ -1,50 +0,0 @@ |
|||||||
package libtrust |
|
||||||
|
|
||||||
import ( |
|
||||||
"path/filepath" |
|
||||||
) |
|
||||||
|
|
||||||
// FilterByHosts filters the list of PublicKeys to only those which contain a
|
|
||||||
// 'hosts' pattern which matches the given host. If *includeEmpty* is true,
|
|
||||||
// then keys which do not specify any hosts are also returned.
|
|
||||||
func FilterByHosts(keys []PublicKey, host string, includeEmpty bool) ([]PublicKey, error) { |
|
||||||
filtered := make([]PublicKey, 0, len(keys)) |
|
||||||
|
|
||||||
for _, pubKey := range keys { |
|
||||||
var hosts []string |
|
||||||
switch v := pubKey.GetExtendedField("hosts").(type) { |
|
||||||
case []string: |
|
||||||
hosts = v |
|
||||||
case []interface{}: |
|
||||||
for _, value := range v { |
|
||||||
h, ok := value.(string) |
|
||||||
if !ok { |
|
||||||
continue |
|
||||||
} |
|
||||||
hosts = append(hosts, h) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if len(hosts) == 0 { |
|
||||||
if includeEmpty { |
|
||||||
filtered = append(filtered, pubKey) |
|
||||||
} |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
// Check if any hosts match pattern
|
|
||||||
for _, hostPattern := range hosts { |
|
||||||
match, err := filepath.Match(hostPattern, host) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
if match { |
|
||||||
filtered = append(filtered, pubKey) |
|
||||||
continue |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return filtered, nil |
|
||||||
} |
|
@ -1,56 +0,0 @@ |
|||||||
package libtrust |
|
||||||
|
|
||||||
import ( |
|
||||||
"crypto" |
|
||||||
_ "crypto/sha256" // Registrer SHA224 and SHA256
|
|
||||||
_ "crypto/sha512" // Registrer SHA384 and SHA512
|
|
||||||
"fmt" |
|
||||||
) |
|
||||||
|
|
||||||
type signatureAlgorithm struct { |
|
||||||
algHeaderParam string |
|
||||||
hashID crypto.Hash |
|
||||||
} |
|
||||||
|
|
||||||
func (h *signatureAlgorithm) HeaderParam() string { |
|
||||||
return h.algHeaderParam |
|
||||||
} |
|
||||||
|
|
||||||
func (h *signatureAlgorithm) HashID() crypto.Hash { |
|
||||||
return h.hashID |
|
||||||
} |
|
||||||
|
|
||||||
var ( |
|
||||||
rs256 = &signatureAlgorithm{"RS256", crypto.SHA256} |
|
||||||
rs384 = &signatureAlgorithm{"RS384", crypto.SHA384} |
|
||||||
rs512 = &signatureAlgorithm{"RS512", crypto.SHA512} |
|
||||||
es256 = &signatureAlgorithm{"ES256", crypto.SHA256} |
|
||||||
es384 = &signatureAlgorithm{"ES384", crypto.SHA384} |
|
||||||
es512 = &signatureAlgorithm{"ES512", crypto.SHA512} |
|
||||||
) |
|
||||||
|
|
||||||
func rsaSignatureAlgorithmByName(alg string) (*signatureAlgorithm, error) { |
|
||||||
switch { |
|
||||||
case alg == "RS256": |
|
||||||
return rs256, nil |
|
||||||
case alg == "RS384": |
|
||||||
return rs384, nil |
|
||||||
case alg == "RS512": |
|
||||||
return rs512, nil |
|
||||||
default: |
|
||||||
return nil, fmt.Errorf("RSA Digital Signature Algorithm %q not supported", alg) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func rsaPKCS1v15SignatureAlgorithmForHashID(hashID crypto.Hash) *signatureAlgorithm { |
|
||||||
switch { |
|
||||||
case hashID == crypto.SHA512: |
|
||||||
return rs512 |
|
||||||
case hashID == crypto.SHA384: |
|
||||||
return rs384 |
|
||||||
case hashID == crypto.SHA256: |
|
||||||
fallthrough |
|
||||||
default: |
|
||||||
return rs256 |
|
||||||
} |
|
||||||
} |
|
@ -1,657 +0,0 @@ |
|||||||
package libtrust |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"crypto" |
|
||||||
"crypto/x509" |
|
||||||
"encoding/base64" |
|
||||||
"encoding/json" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"sort" |
|
||||||
"time" |
|
||||||
"unicode" |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
|
||||||
// ErrInvalidSignContent is used when the content to be signed is invalid.
|
|
||||||
ErrInvalidSignContent = errors.New("invalid sign content") |
|
||||||
|
|
||||||
// ErrInvalidJSONContent is used when invalid json is encountered.
|
|
||||||
ErrInvalidJSONContent = errors.New("invalid json content") |
|
||||||
|
|
||||||
// ErrMissingSignatureKey is used when the specified signature key
|
|
||||||
// does not exist in the JSON content.
|
|
||||||
ErrMissingSignatureKey = errors.New("missing signature key") |
|
||||||
) |
|
||||||
|
|
||||||
type jsHeader struct { |
|
||||||
JWK PublicKey `json:"jwk,omitempty"` |
|
||||||
Algorithm string `json:"alg"` |
|
||||||
Chain []string `json:"x5c,omitempty"` |
|
||||||
} |
|
||||||
|
|
||||||
type jsSignature struct { |
|
||||||
Header jsHeader `json:"header"` |
|
||||||
Signature string `json:"signature"` |
|
||||||
Protected string `json:"protected,omitempty"` |
|
||||||
} |
|
||||||
|
|
||||||
type jsSignaturesSorted []jsSignature |
|
||||||
|
|
||||||
func (jsbkid jsSignaturesSorted) Swap(i, j int) { jsbkid[i], jsbkid[j] = jsbkid[j], jsbkid[i] } |
|
||||||
func (jsbkid jsSignaturesSorted) Len() int { return len(jsbkid) } |
|
||||||
|
|
||||||
func (jsbkid jsSignaturesSorted) Less(i, j int) bool { |
|
||||||
ki, kj := jsbkid[i].Header.JWK.KeyID(), jsbkid[j].Header.JWK.KeyID() |
|
||||||
si, sj := jsbkid[i].Signature, jsbkid[j].Signature |
|
||||||
|
|
||||||
if ki == kj { |
|
||||||
return si < sj |
|
||||||
} |
|
||||||
|
|
||||||
return ki < kj |
|
||||||
} |
|
||||||
|
|
||||||
type signKey struct { |
|
||||||
PrivateKey |
|
||||||
Chain []*x509.Certificate |
|
||||||
} |
|
||||||
|
|
||||||
// JSONSignature represents a signature of a json object.
|
|
||||||
type JSONSignature struct { |
|
||||||
payload string |
|
||||||
signatures []jsSignature |
|
||||||
indent string |
|
||||||
formatLength int |
|
||||||
formatTail []byte |
|
||||||
} |
|
||||||
|
|
||||||
func newJSONSignature() *JSONSignature { |
|
||||||
return &JSONSignature{ |
|
||||||
signatures: make([]jsSignature, 0, 1), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Payload returns the encoded payload of the signature. This
|
|
||||||
// payload should not be signed directly
|
|
||||||
func (js *JSONSignature) Payload() ([]byte, error) { |
|
||||||
return joseBase64UrlDecode(js.payload) |
|
||||||
} |
|
||||||
|
|
||||||
func (js *JSONSignature) protectedHeader() (string, error) { |
|
||||||
protected := map[string]interface{}{ |
|
||||||
"formatLength": js.formatLength, |
|
||||||
"formatTail": joseBase64UrlEncode(js.formatTail), |
|
||||||
"time": time.Now().UTC().Format(time.RFC3339), |
|
||||||
} |
|
||||||
protectedBytes, err := json.Marshal(protected) |
|
||||||
if err != nil { |
|
||||||
return "", err |
|
||||||
} |
|
||||||
|
|
||||||
return joseBase64UrlEncode(protectedBytes), nil |
|
||||||
} |
|
||||||
|
|
||||||
func (js *JSONSignature) signBytes(protectedHeader string) ([]byte, error) { |
|
||||||
buf := make([]byte, len(js.payload)+len(protectedHeader)+1) |
|
||||||
copy(buf, protectedHeader) |
|
||||||
buf[len(protectedHeader)] = '.' |
|
||||||
copy(buf[len(protectedHeader)+1:], js.payload) |
|
||||||
return buf, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Sign adds a signature using the given private key.
|
|
||||||
func (js *JSONSignature) Sign(key PrivateKey) error { |
|
||||||
protected, err := js.protectedHeader() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
signBytes, err := js.signBytes(protected) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
sigBytes, algorithm, err := key.Sign(bytes.NewReader(signBytes), crypto.SHA256) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
js.signatures = append(js.signatures, jsSignature{ |
|
||||||
Header: jsHeader{ |
|
||||||
JWK: key.PublicKey(), |
|
||||||
Algorithm: algorithm, |
|
||||||
}, |
|
||||||
Signature: joseBase64UrlEncode(sigBytes), |
|
||||||
Protected: protected, |
|
||||||
}) |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// SignWithChain adds a signature using the given private key
|
|
||||||
// and setting the x509 chain. The public key of the first element
|
|
||||||
// in the chain must be the public key corresponding with the sign key.
|
|
||||||
func (js *JSONSignature) SignWithChain(key PrivateKey, chain []*x509.Certificate) error { |
|
||||||
// Ensure key.Chain[0] is public key for key
|
|
||||||
//key.Chain.PublicKey
|
|
||||||
//key.PublicKey().CryptoPublicKey()
|
|
||||||
|
|
||||||
// Verify chain
|
|
||||||
protected, err := js.protectedHeader() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
signBytes, err := js.signBytes(protected) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
sigBytes, algorithm, err := key.Sign(bytes.NewReader(signBytes), crypto.SHA256) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
header := jsHeader{ |
|
||||||
Chain: make([]string, len(chain)), |
|
||||||
Algorithm: algorithm, |
|
||||||
} |
|
||||||
|
|
||||||
for i, cert := range chain { |
|
||||||
header.Chain[i] = base64.StdEncoding.EncodeToString(cert.Raw) |
|
||||||
} |
|
||||||
|
|
||||||
js.signatures = append(js.signatures, jsSignature{ |
|
||||||
Header: header, |
|
||||||
Signature: joseBase64UrlEncode(sigBytes), |
|
||||||
Protected: protected, |
|
||||||
}) |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Verify verifies all the signatures and returns the list of
|
|
||||||
// public keys used to sign. Any x509 chains are not checked.
|
|
||||||
func (js *JSONSignature) Verify() ([]PublicKey, error) { |
|
||||||
keys := make([]PublicKey, len(js.signatures)) |
|
||||||
for i, signature := range js.signatures { |
|
||||||
signBytes, err := js.signBytes(signature.Protected) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
var publicKey PublicKey |
|
||||||
if len(signature.Header.Chain) > 0 { |
|
||||||
certBytes, err := base64.StdEncoding.DecodeString(signature.Header.Chain[0]) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
cert, err := x509.ParseCertificate(certBytes) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
publicKey, err = FromCryptoPublicKey(cert.PublicKey) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
} else if signature.Header.JWK != nil { |
|
||||||
publicKey = signature.Header.JWK |
|
||||||
} else { |
|
||||||
return nil, errors.New("missing public key") |
|
||||||
} |
|
||||||
|
|
||||||
sigBytes, err := joseBase64UrlDecode(signature.Signature) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
err = publicKey.Verify(bytes.NewReader(signBytes), signature.Header.Algorithm, sigBytes) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
keys[i] = publicKey |
|
||||||
} |
|
||||||
return keys, nil |
|
||||||
} |
|
||||||
|
|
||||||
// VerifyChains verifies all the signatures and the chains associated
|
|
||||||
// with each signature and returns the list of verified chains.
|
|
||||||
// Signatures without an x509 chain are not checked.
|
|
||||||
func (js *JSONSignature) VerifyChains(ca *x509.CertPool) ([][]*x509.Certificate, error) { |
|
||||||
chains := make([][]*x509.Certificate, 0, len(js.signatures)) |
|
||||||
for _, signature := range js.signatures { |
|
||||||
signBytes, err := js.signBytes(signature.Protected) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
var publicKey PublicKey |
|
||||||
if len(signature.Header.Chain) > 0 { |
|
||||||
certBytes, err := base64.StdEncoding.DecodeString(signature.Header.Chain[0]) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
cert, err := x509.ParseCertificate(certBytes) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
publicKey, err = FromCryptoPublicKey(cert.PublicKey) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
intermediates := x509.NewCertPool() |
|
||||||
if len(signature.Header.Chain) > 1 { |
|
||||||
intermediateChain := signature.Header.Chain[1:] |
|
||||||
for i := range intermediateChain { |
|
||||||
certBytes, err := base64.StdEncoding.DecodeString(intermediateChain[i]) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
intermediate, err := x509.ParseCertificate(certBytes) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
intermediates.AddCert(intermediate) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
verifyOptions := x509.VerifyOptions{ |
|
||||||
Intermediates: intermediates, |
|
||||||
Roots: ca, |
|
||||||
} |
|
||||||
|
|
||||||
verifiedChains, err := cert.Verify(verifyOptions) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
chains = append(chains, verifiedChains...) |
|
||||||
|
|
||||||
sigBytes, err := joseBase64UrlDecode(signature.Signature) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
err = publicKey.Verify(bytes.NewReader(signBytes), signature.Header.Algorithm, sigBytes) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
return chains, nil |
|
||||||
} |
|
||||||
|
|
||||||
// JWS returns JSON serialized JWS according to
|
|
||||||
// http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-7.2
|
|
||||||
func (js *JSONSignature) JWS() ([]byte, error) { |
|
||||||
if len(js.signatures) == 0 { |
|
||||||
return nil, errors.New("missing signature") |
|
||||||
} |
|
||||||
|
|
||||||
sort.Sort(jsSignaturesSorted(js.signatures)) |
|
||||||
|
|
||||||
jsonMap := map[string]interface{}{ |
|
||||||
"payload": js.payload, |
|
||||||
"signatures": js.signatures, |
|
||||||
} |
|
||||||
|
|
||||||
return json.MarshalIndent(jsonMap, "", " ") |
|
||||||
} |
|
||||||
|
|
||||||
func notSpace(r rune) bool { |
|
||||||
return !unicode.IsSpace(r) |
|
||||||
} |
|
||||||
|
|
||||||
func detectJSONIndent(jsonContent []byte) (indent string) { |
|
||||||
if len(jsonContent) > 2 && jsonContent[0] == '{' && jsonContent[1] == '\n' { |
|
||||||
quoteIndex := bytes.IndexRune(jsonContent[1:], '"') |
|
||||||
if quoteIndex > 0 { |
|
||||||
indent = string(jsonContent[2 : quoteIndex+1]) |
|
||||||
} |
|
||||||
} |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
type jsParsedHeader struct { |
|
||||||
JWK json.RawMessage `json:"jwk"` |
|
||||||
Algorithm string `json:"alg"` |
|
||||||
Chain []string `json:"x5c"` |
|
||||||
} |
|
||||||
|
|
||||||
type jsParsedSignature struct { |
|
||||||
Header jsParsedHeader `json:"header"` |
|
||||||
Signature string `json:"signature"` |
|
||||||
Protected string `json:"protected"` |
|
||||||
} |
|
||||||
|
|
||||||
// ParseJWS parses a JWS serialized JSON object into a Json Signature.
|
|
||||||
func ParseJWS(content []byte) (*JSONSignature, error) { |
|
||||||
type jsParsed struct { |
|
||||||
Payload string `json:"payload"` |
|
||||||
Signatures []jsParsedSignature `json:"signatures"` |
|
||||||
} |
|
||||||
parsed := &jsParsed{} |
|
||||||
err := json.Unmarshal(content, parsed) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
if len(parsed.Signatures) == 0 { |
|
||||||
return nil, errors.New("missing signatures") |
|
||||||
} |
|
||||||
payload, err := joseBase64UrlDecode(parsed.Payload) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
js, err := NewJSONSignature(payload) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
js.signatures = make([]jsSignature, len(parsed.Signatures)) |
|
||||||
for i, signature := range parsed.Signatures { |
|
||||||
header := jsHeader{ |
|
||||||
Algorithm: signature.Header.Algorithm, |
|
||||||
} |
|
||||||
if signature.Header.Chain != nil { |
|
||||||
header.Chain = signature.Header.Chain |
|
||||||
} |
|
||||||
if signature.Header.JWK != nil { |
|
||||||
publicKey, err := UnmarshalPublicKeyJWK([]byte(signature.Header.JWK)) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
header.JWK = publicKey |
|
||||||
} |
|
||||||
js.signatures[i] = jsSignature{ |
|
||||||
Header: header, |
|
||||||
Signature: signature.Signature, |
|
||||||
Protected: signature.Protected, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return js, nil |
|
||||||
} |
|
||||||
|
|
||||||
// NewJSONSignature returns a new unsigned JWS from a json byte array.
|
|
||||||
// JSONSignature will need to be signed before serializing or storing.
|
|
||||||
// Optionally, one or more signatures can be provided as byte buffers,
|
|
||||||
// containing serialized JWS signatures, to assemble a fully signed JWS
|
|
||||||
// package. It is the callers responsibility to ensure uniqueness of the
|
|
||||||
// provided signatures.
|
|
||||||
func NewJSONSignature(content []byte, signatures ...[]byte) (*JSONSignature, error) { |
|
||||||
var dataMap map[string]interface{} |
|
||||||
err := json.Unmarshal(content, &dataMap) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
js := newJSONSignature() |
|
||||||
js.indent = detectJSONIndent(content) |
|
||||||
|
|
||||||
js.payload = joseBase64UrlEncode(content) |
|
||||||
|
|
||||||
// Find trailing } and whitespace, put in protected header
|
|
||||||
closeIndex := bytes.LastIndexFunc(content, notSpace) |
|
||||||
if content[closeIndex] != '}' { |
|
||||||
return nil, ErrInvalidJSONContent |
|
||||||
} |
|
||||||
lastRuneIndex := bytes.LastIndexFunc(content[:closeIndex], notSpace) |
|
||||||
if content[lastRuneIndex] == ',' { |
|
||||||
return nil, ErrInvalidJSONContent |
|
||||||
} |
|
||||||
js.formatLength = lastRuneIndex + 1 |
|
||||||
js.formatTail = content[js.formatLength:] |
|
||||||
|
|
||||||
if len(signatures) > 0 { |
|
||||||
for _, signature := range signatures { |
|
||||||
var parsedJSig jsParsedSignature |
|
||||||
|
|
||||||
if err := json.Unmarshal(signature, &parsedJSig); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
// TODO(stevvooe): A lot of the code below is repeated in
|
|
||||||
// ParseJWS. It will require more refactoring to fix that.
|
|
||||||
jsig := jsSignature{ |
|
||||||
Header: jsHeader{ |
|
||||||
Algorithm: parsedJSig.Header.Algorithm, |
|
||||||
}, |
|
||||||
Signature: parsedJSig.Signature, |
|
||||||
Protected: parsedJSig.Protected, |
|
||||||
} |
|
||||||
|
|
||||||
if parsedJSig.Header.Chain != nil { |
|
||||||
jsig.Header.Chain = parsedJSig.Header.Chain |
|
||||||
} |
|
||||||
|
|
||||||
if parsedJSig.Header.JWK != nil { |
|
||||||
publicKey, err := UnmarshalPublicKeyJWK([]byte(parsedJSig.Header.JWK)) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
jsig.Header.JWK = publicKey |
|
||||||
} |
|
||||||
|
|
||||||
js.signatures = append(js.signatures, jsig) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return js, nil |
|
||||||
} |
|
||||||
|
|
||||||
// NewJSONSignatureFromMap returns a new unsigned JSONSignature from a map or
|
|
||||||
// struct. JWS will need to be signed before serializing or storing.
|
|
||||||
func NewJSONSignatureFromMap(content interface{}) (*JSONSignature, error) { |
|
||||||
switch content.(type) { |
|
||||||
case map[string]interface{}: |
|
||||||
case struct{}: |
|
||||||
default: |
|
||||||
return nil, errors.New("invalid data type") |
|
||||||
} |
|
||||||
|
|
||||||
js := newJSONSignature() |
|
||||||
js.indent = " " |
|
||||||
|
|
||||||
payload, err := json.MarshalIndent(content, "", js.indent) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
js.payload = joseBase64UrlEncode(payload) |
|
||||||
|
|
||||||
// Remove '\n}' from formatted section, put in protected header
|
|
||||||
js.formatLength = len(payload) - 2 |
|
||||||
js.formatTail = payload[js.formatLength:] |
|
||||||
|
|
||||||
return js, nil |
|
||||||
} |
|
||||||
|
|
||||||
func readIntFromMap(key string, m map[string]interface{}) (int, bool) { |
|
||||||
value, ok := m[key] |
|
||||||
if !ok { |
|
||||||
return 0, false |
|
||||||
} |
|
||||||
switch v := value.(type) { |
|
||||||
case int: |
|
||||||
return v, true |
|
||||||
case float64: |
|
||||||
return int(v), true |
|
||||||
default: |
|
||||||
return 0, false |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func readStringFromMap(key string, m map[string]interface{}) (v string, ok bool) { |
|
||||||
value, ok := m[key] |
|
||||||
if !ok { |
|
||||||
return "", false |
|
||||||
} |
|
||||||
v, ok = value.(string) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// ParsePrettySignature parses a formatted signature into a
|
|
||||||
// JSON signature. If the signatures are missing the format information
|
|
||||||
// an error is thrown. The formatted signature must be created by
|
|
||||||
// the same method as format signature.
|
|
||||||
func ParsePrettySignature(content []byte, signatureKey string) (*JSONSignature, error) { |
|
||||||
var contentMap map[string]json.RawMessage |
|
||||||
err := json.Unmarshal(content, &contentMap) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("error unmarshalling content: %s", err) |
|
||||||
} |
|
||||||
sigMessage, ok := contentMap[signatureKey] |
|
||||||
if !ok { |
|
||||||
return nil, ErrMissingSignatureKey |
|
||||||
} |
|
||||||
|
|
||||||
var signatureBlocks []jsParsedSignature |
|
||||||
err = json.Unmarshal([]byte(sigMessage), &signatureBlocks) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("error unmarshalling signatures: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
js := newJSONSignature() |
|
||||||
js.signatures = make([]jsSignature, len(signatureBlocks)) |
|
||||||
|
|
||||||
for i, signatureBlock := range signatureBlocks { |
|
||||||
protectedBytes, err := joseBase64UrlDecode(signatureBlock.Protected) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("base64 decode error: %s", err) |
|
||||||
} |
|
||||||
var protectedHeader map[string]interface{} |
|
||||||
err = json.Unmarshal(protectedBytes, &protectedHeader) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("error unmarshalling protected header: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
formatLength, ok := readIntFromMap("formatLength", protectedHeader) |
|
||||||
if !ok { |
|
||||||
return nil, errors.New("missing formatted length") |
|
||||||
} |
|
||||||
encodedTail, ok := readStringFromMap("formatTail", protectedHeader) |
|
||||||
if !ok { |
|
||||||
return nil, errors.New("missing formatted tail") |
|
||||||
} |
|
||||||
formatTail, err := joseBase64UrlDecode(encodedTail) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("base64 decode error on tail: %s", err) |
|
||||||
} |
|
||||||
if js.formatLength == 0 { |
|
||||||
js.formatLength = formatLength |
|
||||||
} else if js.formatLength != formatLength { |
|
||||||
return nil, errors.New("conflicting format length") |
|
||||||
} |
|
||||||
if len(js.formatTail) == 0 { |
|
||||||
js.formatTail = formatTail |
|
||||||
} else if bytes.Compare(js.formatTail, formatTail) != 0 { |
|
||||||
return nil, errors.New("conflicting format tail") |
|
||||||
} |
|
||||||
|
|
||||||
header := jsHeader{ |
|
||||||
Algorithm: signatureBlock.Header.Algorithm, |
|
||||||
Chain: signatureBlock.Header.Chain, |
|
||||||
} |
|
||||||
if signatureBlock.Header.JWK != nil { |
|
||||||
publicKey, err := UnmarshalPublicKeyJWK([]byte(signatureBlock.Header.JWK)) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("error unmarshalling public key: %s", err) |
|
||||||
} |
|
||||||
header.JWK = publicKey |
|
||||||
} |
|
||||||
js.signatures[i] = jsSignature{ |
|
||||||
Header: header, |
|
||||||
Signature: signatureBlock.Signature, |
|
||||||
Protected: signatureBlock.Protected, |
|
||||||
} |
|
||||||
} |
|
||||||
if js.formatLength > len(content) { |
|
||||||
return nil, errors.New("invalid format length") |
|
||||||
} |
|
||||||
formatted := make([]byte, js.formatLength+len(js.formatTail)) |
|
||||||
copy(formatted, content[:js.formatLength]) |
|
||||||
copy(formatted[js.formatLength:], js.formatTail) |
|
||||||
js.indent = detectJSONIndent(formatted) |
|
||||||
js.payload = joseBase64UrlEncode(formatted) |
|
||||||
|
|
||||||
return js, nil |
|
||||||
} |
|
||||||
|
|
||||||
// PrettySignature formats a json signature into an easy to read
|
|
||||||
// single json serialized object.
|
|
||||||
func (js *JSONSignature) PrettySignature(signatureKey string) ([]byte, error) { |
|
||||||
if len(js.signatures) == 0 { |
|
||||||
return nil, errors.New("no signatures") |
|
||||||
} |
|
||||||
payload, err := joseBase64UrlDecode(js.payload) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
payload = payload[:js.formatLength] |
|
||||||
|
|
||||||
sort.Sort(jsSignaturesSorted(js.signatures)) |
|
||||||
|
|
||||||
var marshalled []byte |
|
||||||
var marshallErr error |
|
||||||
if js.indent != "" { |
|
||||||
marshalled, marshallErr = json.MarshalIndent(js.signatures, js.indent, js.indent) |
|
||||||
} else { |
|
||||||
marshalled, marshallErr = json.Marshal(js.signatures) |
|
||||||
} |
|
||||||
if marshallErr != nil { |
|
||||||
return nil, marshallErr |
|
||||||
} |
|
||||||
|
|
||||||
buf := bytes.NewBuffer(make([]byte, 0, len(payload)+len(marshalled)+34)) |
|
||||||
buf.Write(payload) |
|
||||||
buf.WriteByte(',') |
|
||||||
if js.indent != "" { |
|
||||||
buf.WriteByte('\n') |
|
||||||
buf.WriteString(js.indent) |
|
||||||
buf.WriteByte('"') |
|
||||||
buf.WriteString(signatureKey) |
|
||||||
buf.WriteString("\": ") |
|
||||||
buf.Write(marshalled) |
|
||||||
buf.WriteByte('\n') |
|
||||||
} else { |
|
||||||
buf.WriteByte('"') |
|
||||||
buf.WriteString(signatureKey) |
|
||||||
buf.WriteString("\":") |
|
||||||
buf.Write(marshalled) |
|
||||||
} |
|
||||||
buf.WriteByte('}') |
|
||||||
|
|
||||||
return buf.Bytes(), nil |
|
||||||
} |
|
||||||
|
|
||||||
// Signatures provides the signatures on this JWS as opaque blobs, sorted by
|
|
||||||
// keyID. These blobs can be stored and reassembled with payloads. Internally,
|
|
||||||
// they are simply marshaled json web signatures but implementations should
|
|
||||||
// not rely on this.
|
|
||||||
func (js *JSONSignature) Signatures() ([][]byte, error) { |
|
||||||
sort.Sort(jsSignaturesSorted(js.signatures)) |
|
||||||
|
|
||||||
var sb [][]byte |
|
||||||
for _, jsig := range js.signatures { |
|
||||||
p, err := json.Marshal(jsig) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
sb = append(sb, p) |
|
||||||
} |
|
||||||
|
|
||||||
return sb, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Merge combines the signatures from one or more other signatures into the
|
|
||||||
// method receiver. If the payloads differ for any argument, an error will be
|
|
||||||
// returned and the receiver will not be modified.
|
|
||||||
func (js *JSONSignature) Merge(others ...*JSONSignature) error { |
|
||||||
merged := js.signatures |
|
||||||
for _, other := range others { |
|
||||||
if js.payload != other.payload { |
|
||||||
return fmt.Errorf("payloads differ from merge target") |
|
||||||
} |
|
||||||
merged = append(merged, other.signatures...) |
|
||||||
} |
|
||||||
|
|
||||||
js.signatures = merged |
|
||||||
return nil |
|
||||||
} |
|
@ -1,253 +0,0 @@ |
|||||||
package libtrust |
|
||||||
|
|
||||||
import ( |
|
||||||
"crypto" |
|
||||||
"crypto/ecdsa" |
|
||||||
"crypto/rsa" |
|
||||||
"crypto/x509" |
|
||||||
"encoding/json" |
|
||||||
"encoding/pem" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"io" |
|
||||||
) |
|
||||||
|
|
||||||
// PublicKey is a generic interface for a Public Key.
|
|
||||||
type PublicKey interface { |
|
||||||
// KeyType returns the key type for this key. For elliptic curve keys,
|
|
||||||
// this value should be "EC". For RSA keys, this value should be "RSA".
|
|
||||||
KeyType() string |
|
||||||
// KeyID returns a distinct identifier which is unique to this Public Key.
|
|
||||||
// The format generated by this library is a base32 encoding of a 240 bit
|
|
||||||
// hash of the public key data divided into 12 groups like so:
|
|
||||||
// ABCD:EFGH:IJKL:MNOP:QRST:UVWX:YZ23:4567:ABCD:EFGH:IJKL:MNOP
|
|
||||||
KeyID() string |
|
||||||
// Verify verifyies the signature of the data in the io.Reader using this
|
|
||||||
// Public Key. The alg parameter should identify the digital signature
|
|
||||||
// algorithm which was used to produce the signature and should be
|
|
||||||
// supported by this public key. Returns a nil error if the signature
|
|
||||||
// is valid.
|
|
||||||
Verify(data io.Reader, alg string, signature []byte) error |
|
||||||
// CryptoPublicKey returns the internal object which can be used as a
|
|
||||||
// crypto.PublicKey for use with other standard library operations. The type
|
|
||||||
// is either *rsa.PublicKey or *ecdsa.PublicKey
|
|
||||||
CryptoPublicKey() crypto.PublicKey |
|
||||||
// These public keys can be serialized to the standard JSON encoding for
|
|
||||||
// JSON Web Keys. See section 6 of the IETF draft RFC for JOSE JSON Web
|
|
||||||
// Algorithms.
|
|
||||||
MarshalJSON() ([]byte, error) |
|
||||||
// These keys can also be serialized to the standard PEM encoding.
|
|
||||||
PEMBlock() (*pem.Block, error) |
|
||||||
// The string representation of a key is its key type and ID.
|
|
||||||
String() string |
|
||||||
AddExtendedField(string, interface{}) |
|
||||||
GetExtendedField(string) interface{} |
|
||||||
} |
|
||||||
|
|
||||||
// PrivateKey is a generic interface for a Private Key.
|
|
||||||
type PrivateKey interface { |
|
||||||
// A PrivateKey contains all fields and methods of a PublicKey of the
|
|
||||||
// same type. The MarshalJSON method also outputs the private key as a
|
|
||||||
// JSON Web Key, and the PEMBlock method outputs the private key as a
|
|
||||||
// PEM block.
|
|
||||||
PublicKey |
|
||||||
// PublicKey returns the PublicKey associated with this PrivateKey.
|
|
||||||
PublicKey() PublicKey |
|
||||||
// Sign signs the data read from the io.Reader using a signature algorithm
|
|
||||||
// supported by the private key. If the specified hashing algorithm is
|
|
||||||
// supported by this key, that hash function is used to generate the
|
|
||||||
// signature otherwise the the default hashing algorithm for this key is
|
|
||||||
// used. Returns the signature and identifier of the algorithm used.
|
|
||||||
Sign(data io.Reader, hashID crypto.Hash) (signature []byte, alg string, err error) |
|
||||||
// CryptoPrivateKey returns the internal object which can be used as a
|
|
||||||
// crypto.PublicKey for use with other standard library operations. The
|
|
||||||
// type is either *rsa.PublicKey or *ecdsa.PublicKey
|
|
||||||
CryptoPrivateKey() crypto.PrivateKey |
|
||||||
} |
|
||||||
|
|
||||||
// FromCryptoPublicKey returns a libtrust PublicKey representation of the given
|
|
||||||
// *ecdsa.PublicKey or *rsa.PublicKey. Returns a non-nil error when the given
|
|
||||||
// key is of an unsupported type.
|
|
||||||
func FromCryptoPublicKey(cryptoPublicKey crypto.PublicKey) (PublicKey, error) { |
|
||||||
switch cryptoPublicKey := cryptoPublicKey.(type) { |
|
||||||
case *ecdsa.PublicKey: |
|
||||||
return fromECPublicKey(cryptoPublicKey) |
|
||||||
case *rsa.PublicKey: |
|
||||||
return fromRSAPublicKey(cryptoPublicKey), nil |
|
||||||
default: |
|
||||||
return nil, fmt.Errorf("public key type %T is not supported", cryptoPublicKey) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// FromCryptoPrivateKey returns a libtrust PrivateKey representation of the given
|
|
||||||
// *ecdsa.PrivateKey or *rsa.PrivateKey. Returns a non-nil error when the given
|
|
||||||
// key is of an unsupported type.
|
|
||||||
func FromCryptoPrivateKey(cryptoPrivateKey crypto.PrivateKey) (PrivateKey, error) { |
|
||||||
switch cryptoPrivateKey := cryptoPrivateKey.(type) { |
|
||||||
case *ecdsa.PrivateKey: |
|
||||||
return fromECPrivateKey(cryptoPrivateKey) |
|
||||||
case *rsa.PrivateKey: |
|
||||||
return fromRSAPrivateKey(cryptoPrivateKey), nil |
|
||||||
default: |
|
||||||
return nil, fmt.Errorf("private key type %T is not supported", cryptoPrivateKey) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// UnmarshalPublicKeyPEM parses the PEM encoded data and returns a libtrust
|
|
||||||
// PublicKey or an error if there is a problem with the encoding.
|
|
||||||
func UnmarshalPublicKeyPEM(data []byte) (PublicKey, error) { |
|
||||||
pemBlock, _ := pem.Decode(data) |
|
||||||
if pemBlock == nil { |
|
||||||
return nil, errors.New("unable to find PEM encoded data") |
|
||||||
} else if pemBlock.Type != "PUBLIC KEY" { |
|
||||||
return nil, fmt.Errorf("unable to get PublicKey from PEM type: %s", pemBlock.Type) |
|
||||||
} |
|
||||||
|
|
||||||
return pubKeyFromPEMBlock(pemBlock) |
|
||||||
} |
|
||||||
|
|
||||||
// UnmarshalPublicKeyPEMBundle parses the PEM encoded data as a bundle of
|
|
||||||
// PEM blocks appended one after the other and returns a slice of PublicKey
|
|
||||||
// objects that it finds.
|
|
||||||
func UnmarshalPublicKeyPEMBundle(data []byte) ([]PublicKey, error) { |
|
||||||
pubKeys := []PublicKey{} |
|
||||||
|
|
||||||
for { |
|
||||||
var pemBlock *pem.Block |
|
||||||
pemBlock, data = pem.Decode(data) |
|
||||||
if pemBlock == nil { |
|
||||||
break |
|
||||||
} else if pemBlock.Type != "PUBLIC KEY" { |
|
||||||
return nil, fmt.Errorf("unable to get PublicKey from PEM type: %s", pemBlock.Type) |
|
||||||
} |
|
||||||
|
|
||||||
pubKey, err := pubKeyFromPEMBlock(pemBlock) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
pubKeys = append(pubKeys, pubKey) |
|
||||||
} |
|
||||||
|
|
||||||
return pubKeys, nil |
|
||||||
} |
|
||||||
|
|
||||||
// UnmarshalPrivateKeyPEM parses the PEM encoded data and returns a libtrust
|
|
||||||
// PrivateKey or an error if there is a problem with the encoding.
|
|
||||||
func UnmarshalPrivateKeyPEM(data []byte) (PrivateKey, error) { |
|
||||||
pemBlock, _ := pem.Decode(data) |
|
||||||
if pemBlock == nil { |
|
||||||
return nil, errors.New("unable to find PEM encoded data") |
|
||||||
} |
|
||||||
|
|
||||||
var key PrivateKey |
|
||||||
|
|
||||||
switch { |
|
||||||
case pemBlock.Type == "RSA PRIVATE KEY": |
|
||||||
rsaPrivateKey, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("unable to decode RSA Private Key PEM data: %s", err) |
|
||||||
} |
|
||||||
key = fromRSAPrivateKey(rsaPrivateKey) |
|
||||||
case pemBlock.Type == "EC PRIVATE KEY": |
|
||||||
ecPrivateKey, err := x509.ParseECPrivateKey(pemBlock.Bytes) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("unable to decode EC Private Key PEM data: %s", err) |
|
||||||
} |
|
||||||
key, err = fromECPrivateKey(ecPrivateKey) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
default: |
|
||||||
return nil, fmt.Errorf("unable to get PrivateKey from PEM type: %s", pemBlock.Type) |
|
||||||
} |
|
||||||
|
|
||||||
addPEMHeadersToKey(pemBlock, key.PublicKey()) |
|
||||||
|
|
||||||
return key, nil |
|
||||||
} |
|
||||||
|
|
||||||
// UnmarshalPublicKeyJWK unmarshals the given JSON Web Key into a generic
|
|
||||||
// Public Key to be used with libtrust.
|
|
||||||
func UnmarshalPublicKeyJWK(data []byte) (PublicKey, error) { |
|
||||||
jwk := make(map[string]interface{}) |
|
||||||
|
|
||||||
err := json.Unmarshal(data, &jwk) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf( |
|
||||||
"decoding JWK Public Key JSON data: %s\n", err, |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
// Get the Key Type value.
|
|
||||||
kty, err := stringFromMap(jwk, "kty") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK Public Key type: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
switch { |
|
||||||
case kty == "EC": |
|
||||||
// Call out to unmarshal EC public key.
|
|
||||||
return ecPublicKeyFromMap(jwk) |
|
||||||
case kty == "RSA": |
|
||||||
// Call out to unmarshal RSA public key.
|
|
||||||
return rsaPublicKeyFromMap(jwk) |
|
||||||
default: |
|
||||||
return nil, fmt.Errorf( |
|
||||||
"JWK Public Key type not supported: %q\n", kty, |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// UnmarshalPublicKeyJWKSet parses the JSON encoded data as a JSON Web Key Set
|
|
||||||
// and returns a slice of Public Key objects.
|
|
||||||
func UnmarshalPublicKeyJWKSet(data []byte) ([]PublicKey, error) { |
|
||||||
rawKeys, err := loadJSONKeySetRaw(data) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
pubKeys := make([]PublicKey, 0, len(rawKeys)) |
|
||||||
|
|
||||||
for _, rawKey := range rawKeys { |
|
||||||
pubKey, err := UnmarshalPublicKeyJWK(rawKey) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
pubKeys = append(pubKeys, pubKey) |
|
||||||
} |
|
||||||
|
|
||||||
return pubKeys, nil |
|
||||||
} |
|
||||||
|
|
||||||
// UnmarshalPrivateKeyJWK unmarshals the given JSON Web Key into a generic
|
|
||||||
// Private Key to be used with libtrust.
|
|
||||||
func UnmarshalPrivateKeyJWK(data []byte) (PrivateKey, error) { |
|
||||||
jwk := make(map[string]interface{}) |
|
||||||
|
|
||||||
err := json.Unmarshal(data, &jwk) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf( |
|
||||||
"decoding JWK Private Key JSON data: %s\n", err, |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
// Get the Key Type value.
|
|
||||||
kty, err := stringFromMap(jwk, "kty") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK Private Key type: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
switch { |
|
||||||
case kty == "EC": |
|
||||||
// Call out to unmarshal EC private key.
|
|
||||||
return ecPrivateKeyFromMap(jwk) |
|
||||||
case kty == "RSA": |
|
||||||
// Call out to unmarshal RSA private key.
|
|
||||||
return rsaPrivateKeyFromMap(jwk) |
|
||||||
default: |
|
||||||
return nil, fmt.Errorf( |
|
||||||
"JWK Private Key type not supported: %q\n", kty, |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
@ -1,255 +0,0 @@ |
|||||||
package libtrust |
|
||||||
|
|
||||||
import ( |
|
||||||
"encoding/json" |
|
||||||
"encoding/pem" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"io/ioutil" |
|
||||||
"os" |
|
||||||
"strings" |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
|
||||||
// ErrKeyFileDoesNotExist indicates that the private key file does not exist.
|
|
||||||
ErrKeyFileDoesNotExist = errors.New("key file does not exist") |
|
||||||
) |
|
||||||
|
|
||||||
func readKeyFileBytes(filename string) ([]byte, error) { |
|
||||||
data, err := ioutil.ReadFile(filename) |
|
||||||
if err != nil { |
|
||||||
if os.IsNotExist(err) { |
|
||||||
err = ErrKeyFileDoesNotExist |
|
||||||
} else { |
|
||||||
err = fmt.Errorf("unable to read key file %s: %s", filename, err) |
|
||||||
} |
|
||||||
|
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return data, nil |
|
||||||
} |
|
||||||
|
|
||||||
/* |
|
||||||
Loading and Saving of Public and Private Keys in either PEM or JWK format. |
|
||||||
*/ |
|
||||||
|
|
||||||
// LoadKeyFile opens the given filename and attempts to read a Private Key
|
|
||||||
// encoded in either PEM or JWK format (if .json or .jwk file extension).
|
|
||||||
func LoadKeyFile(filename string) (PrivateKey, error) { |
|
||||||
contents, err := readKeyFileBytes(filename) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
var key PrivateKey |
|
||||||
|
|
||||||
if strings.HasSuffix(filename, ".json") || strings.HasSuffix(filename, ".jwk") { |
|
||||||
key, err = UnmarshalPrivateKeyJWK(contents) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("unable to decode private key JWK: %s", err) |
|
||||||
} |
|
||||||
} else { |
|
||||||
key, err = UnmarshalPrivateKeyPEM(contents) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("unable to decode private key PEM: %s", err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return key, nil |
|
||||||
} |
|
||||||
|
|
||||||
// LoadPublicKeyFile opens the given filename and attempts to read a Public Key
|
|
||||||
// encoded in either PEM or JWK format (if .json or .jwk file extension).
|
|
||||||
func LoadPublicKeyFile(filename string) (PublicKey, error) { |
|
||||||
contents, err := readKeyFileBytes(filename) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
var key PublicKey |
|
||||||
|
|
||||||
if strings.HasSuffix(filename, ".json") || strings.HasSuffix(filename, ".jwk") { |
|
||||||
key, err = UnmarshalPublicKeyJWK(contents) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("unable to decode public key JWK: %s", err) |
|
||||||
} |
|
||||||
} else { |
|
||||||
key, err = UnmarshalPublicKeyPEM(contents) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("unable to decode public key PEM: %s", err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return key, nil |
|
||||||
} |
|
||||||
|
|
||||||
// SaveKey saves the given key to a file using the provided filename.
|
|
||||||
// This process will overwrite any existing file at the provided location.
|
|
||||||
func SaveKey(filename string, key PrivateKey) error { |
|
||||||
var encodedKey []byte |
|
||||||
var err error |
|
||||||
|
|
||||||
if strings.HasSuffix(filename, ".json") || strings.HasSuffix(filename, ".jwk") { |
|
||||||
// Encode in JSON Web Key format.
|
|
||||||
encodedKey, err = json.MarshalIndent(key, "", " ") |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to encode private key JWK: %s", err) |
|
||||||
} |
|
||||||
} else { |
|
||||||
// Encode in PEM format.
|
|
||||||
pemBlock, err := key.PEMBlock() |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to encode private key PEM: %s", err) |
|
||||||
} |
|
||||||
encodedKey = pem.EncodeToMemory(pemBlock) |
|
||||||
} |
|
||||||
|
|
||||||
err = ioutil.WriteFile(filename, encodedKey, os.FileMode(0600)) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to write private key file %s: %s", filename, err) |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// SavePublicKey saves the given public key to the file.
|
|
||||||
func SavePublicKey(filename string, key PublicKey) error { |
|
||||||
var encodedKey []byte |
|
||||||
var err error |
|
||||||
|
|
||||||
if strings.HasSuffix(filename, ".json") || strings.HasSuffix(filename, ".jwk") { |
|
||||||
// Encode in JSON Web Key format.
|
|
||||||
encodedKey, err = json.MarshalIndent(key, "", " ") |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to encode public key JWK: %s", err) |
|
||||||
} |
|
||||||
} else { |
|
||||||
// Encode in PEM format.
|
|
||||||
pemBlock, err := key.PEMBlock() |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to encode public key PEM: %s", err) |
|
||||||
} |
|
||||||
encodedKey = pem.EncodeToMemory(pemBlock) |
|
||||||
} |
|
||||||
|
|
||||||
err = ioutil.WriteFile(filename, encodedKey, os.FileMode(0644)) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to write public key file %s: %s", filename, err) |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Public Key Set files
|
|
||||||
|
|
||||||
type jwkSet struct { |
|
||||||
Keys []json.RawMessage `json:"keys"` |
|
||||||
} |
|
||||||
|
|
||||||
// LoadKeySetFile loads a key set
|
|
||||||
func LoadKeySetFile(filename string) ([]PublicKey, error) { |
|
||||||
if strings.HasSuffix(filename, ".json") || strings.HasSuffix(filename, ".jwk") { |
|
||||||
return loadJSONKeySetFile(filename) |
|
||||||
} |
|
||||||
|
|
||||||
// Must be a PEM format file
|
|
||||||
return loadPEMKeySetFile(filename) |
|
||||||
} |
|
||||||
|
|
||||||
func loadJSONKeySetRaw(data []byte) ([]json.RawMessage, error) { |
|
||||||
if len(data) == 0 { |
|
||||||
// This is okay, just return an empty slice.
|
|
||||||
return []json.RawMessage{}, nil |
|
||||||
} |
|
||||||
|
|
||||||
keySet := jwkSet{} |
|
||||||
|
|
||||||
err := json.Unmarshal(data, &keySet) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("unable to decode JSON Web Key Set: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
return keySet.Keys, nil |
|
||||||
} |
|
||||||
|
|
||||||
func loadJSONKeySetFile(filename string) ([]PublicKey, error) { |
|
||||||
contents, err := readKeyFileBytes(filename) |
|
||||||
if err != nil && err != ErrKeyFileDoesNotExist { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return UnmarshalPublicKeyJWKSet(contents) |
|
||||||
} |
|
||||||
|
|
||||||
func loadPEMKeySetFile(filename string) ([]PublicKey, error) { |
|
||||||
data, err := readKeyFileBytes(filename) |
|
||||||
if err != nil && err != ErrKeyFileDoesNotExist { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return UnmarshalPublicKeyPEMBundle(data) |
|
||||||
} |
|
||||||
|
|
||||||
// AddKeySetFile adds a key to a key set
|
|
||||||
func AddKeySetFile(filename string, key PublicKey) error { |
|
||||||
if strings.HasSuffix(filename, ".json") || strings.HasSuffix(filename, ".jwk") { |
|
||||||
return addKeySetJSONFile(filename, key) |
|
||||||
} |
|
||||||
|
|
||||||
// Must be a PEM format file
|
|
||||||
return addKeySetPEMFile(filename, key) |
|
||||||
} |
|
||||||
|
|
||||||
func addKeySetJSONFile(filename string, key PublicKey) error { |
|
||||||
encodedKey, err := json.Marshal(key) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to encode trusted client key: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
contents, err := readKeyFileBytes(filename) |
|
||||||
if err != nil && err != ErrKeyFileDoesNotExist { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
rawEntries, err := loadJSONKeySetRaw(contents) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
rawEntries = append(rawEntries, json.RawMessage(encodedKey)) |
|
||||||
entriesWrapper := jwkSet{Keys: rawEntries} |
|
||||||
|
|
||||||
encodedEntries, err := json.MarshalIndent(entriesWrapper, "", " ") |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to encode trusted client keys: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
err = ioutil.WriteFile(filename, encodedEntries, os.FileMode(0644)) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to write trusted client keys file %s: %s", filename, err) |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func addKeySetPEMFile(filename string, key PublicKey) error { |
|
||||||
// Encode to PEM, open file for appending, write PEM.
|
|
||||||
file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, os.FileMode(0644)) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to open trusted client keys file %s: %s", filename, err) |
|
||||||
} |
|
||||||
defer file.Close() |
|
||||||
|
|
||||||
pemBlock, err := key.PEMBlock() |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to encoded trusted key: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
_, err = file.Write(pem.EncodeToMemory(pemBlock)) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to write trusted keys file: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
@ -1,175 +0,0 @@ |
|||||||
package libtrust |
|
||||||
|
|
||||||
import ( |
|
||||||
"crypto/tls" |
|
||||||
"crypto/x509" |
|
||||||
"fmt" |
|
||||||
"io/ioutil" |
|
||||||
"net" |
|
||||||
"os" |
|
||||||
"path" |
|
||||||
"sync" |
|
||||||
) |
|
||||||
|
|
||||||
// ClientKeyManager manages client keys on the filesystem
|
|
||||||
type ClientKeyManager struct { |
|
||||||
key PrivateKey |
|
||||||
clientFile string |
|
||||||
clientDir string |
|
||||||
|
|
||||||
clientLock sync.RWMutex |
|
||||||
clients []PublicKey |
|
||||||
|
|
||||||
configLock sync.Mutex |
|
||||||
configs []*tls.Config |
|
||||||
} |
|
||||||
|
|
||||||
// NewClientKeyManager loads a new manager from a set of key files
|
|
||||||
// and managed by the given private key.
|
|
||||||
func NewClientKeyManager(trustKey PrivateKey, clientFile, clientDir string) (*ClientKeyManager, error) { |
|
||||||
m := &ClientKeyManager{ |
|
||||||
key: trustKey, |
|
||||||
clientFile: clientFile, |
|
||||||
clientDir: clientDir, |
|
||||||
} |
|
||||||
if err := m.loadKeys(); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
// TODO Start watching file and directory
|
|
||||||
|
|
||||||
return m, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (c *ClientKeyManager) loadKeys() (err error) { |
|
||||||
// Load authorized keys file
|
|
||||||
var clients []PublicKey |
|
||||||
if c.clientFile != "" { |
|
||||||
clients, err = LoadKeySetFile(c.clientFile) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to load authorized keys: %s", err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Add clients from authorized keys directory
|
|
||||||
files, err := ioutil.ReadDir(c.clientDir) |
|
||||||
if err != nil && !os.IsNotExist(err) { |
|
||||||
return fmt.Errorf("unable to open authorized keys directory: %s", err) |
|
||||||
} |
|
||||||
for _, f := range files { |
|
||||||
if !f.IsDir() { |
|
||||||
publicKey, err := LoadPublicKeyFile(path.Join(c.clientDir, f.Name())) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to load authorized key file: %s", err) |
|
||||||
} |
|
||||||
clients = append(clients, publicKey) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
c.clientLock.Lock() |
|
||||||
c.clients = clients |
|
||||||
c.clientLock.Unlock() |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// RegisterTLSConfig registers a tls configuration to manager
|
|
||||||
// such that any changes to the keys may be reflected in
|
|
||||||
// the tls client CA pool
|
|
||||||
func (c *ClientKeyManager) RegisterTLSConfig(tlsConfig *tls.Config) error { |
|
||||||
c.clientLock.RLock() |
|
||||||
certPool, err := GenerateCACertPool(c.key, c.clients) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("CA pool generation error: %s", err) |
|
||||||
} |
|
||||||
c.clientLock.RUnlock() |
|
||||||
|
|
||||||
tlsConfig.ClientCAs = certPool |
|
||||||
|
|
||||||
c.configLock.Lock() |
|
||||||
c.configs = append(c.configs, tlsConfig) |
|
||||||
c.configLock.Unlock() |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// NewIdentityAuthTLSConfig creates a tls.Config for the server to use for
|
|
||||||
// libtrust identity authentication for the domain specified
|
|
||||||
func NewIdentityAuthTLSConfig(trustKey PrivateKey, clients *ClientKeyManager, addr string, domain string) (*tls.Config, error) { |
|
||||||
tlsConfig := newTLSConfig() |
|
||||||
|
|
||||||
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert |
|
||||||
if err := clients.RegisterTLSConfig(tlsConfig); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
// Generate cert
|
|
||||||
ips, domains, err := parseAddr(addr) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
// add domain that it expects clients to use
|
|
||||||
domains = append(domains, domain) |
|
||||||
x509Cert, err := GenerateSelfSignedServerCert(trustKey, domains, ips) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("certificate generation error: %s", err) |
|
||||||
} |
|
||||||
tlsConfig.Certificates = []tls.Certificate{{ |
|
||||||
Certificate: [][]byte{x509Cert.Raw}, |
|
||||||
PrivateKey: trustKey.CryptoPrivateKey(), |
|
||||||
Leaf: x509Cert, |
|
||||||
}} |
|
||||||
|
|
||||||
return tlsConfig, nil |
|
||||||
} |
|
||||||
|
|
||||||
// NewCertAuthTLSConfig creates a tls.Config for the server to use for
|
|
||||||
// certificate authentication
|
|
||||||
func NewCertAuthTLSConfig(caPath, certPath, keyPath string) (*tls.Config, error) { |
|
||||||
tlsConfig := newTLSConfig() |
|
||||||
|
|
||||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("Couldn't load X509 key pair (%s, %s): %s. Key encrypted?", certPath, keyPath, err) |
|
||||||
} |
|
||||||
tlsConfig.Certificates = []tls.Certificate{cert} |
|
||||||
|
|
||||||
// Verify client certificates against a CA?
|
|
||||||
if caPath != "" { |
|
||||||
certPool := x509.NewCertPool() |
|
||||||
file, err := ioutil.ReadFile(caPath) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("Couldn't read CA certificate: %s", err) |
|
||||||
} |
|
||||||
certPool.AppendCertsFromPEM(file) |
|
||||||
|
|
||||||
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert |
|
||||||
tlsConfig.ClientCAs = certPool |
|
||||||
} |
|
||||||
|
|
||||||
return tlsConfig, nil |
|
||||||
} |
|
||||||
|
|
||||||
func newTLSConfig() *tls.Config { |
|
||||||
return &tls.Config{ |
|
||||||
NextProtos: []string{"http/1.1"}, |
|
||||||
// Avoid fallback on insecure SSL protocols
|
|
||||||
MinVersion: tls.VersionTLS10, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// parseAddr parses an address into an array of IPs and domains
|
|
||||||
func parseAddr(addr string) ([]net.IP, []string, error) { |
|
||||||
host, _, err := net.SplitHostPort(addr) |
|
||||||
if err != nil { |
|
||||||
return nil, nil, err |
|
||||||
} |
|
||||||
var domains []string |
|
||||||
var ips []net.IP |
|
||||||
ip := net.ParseIP(host) |
|
||||||
if ip != nil { |
|
||||||
ips = []net.IP{ip} |
|
||||||
} else { |
|
||||||
domains = []string{host} |
|
||||||
} |
|
||||||
return ips, domains, nil |
|
||||||
} |
|
@ -1,427 +0,0 @@ |
|||||||
package libtrust |
|
||||||
|
|
||||||
import ( |
|
||||||
"crypto" |
|
||||||
"crypto/rand" |
|
||||||
"crypto/rsa" |
|
||||||
"crypto/x509" |
|
||||||
"encoding/json" |
|
||||||
"encoding/pem" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"io" |
|
||||||
"math/big" |
|
||||||
) |
|
||||||
|
|
||||||
/* |
|
||||||
* RSA DSA PUBLIC KEY |
|
||||||
*/ |
|
||||||
|
|
||||||
// rsaPublicKey implements a JWK Public Key using RSA digital signature algorithms.
|
|
||||||
type rsaPublicKey struct { |
|
||||||
*rsa.PublicKey |
|
||||||
extended map[string]interface{} |
|
||||||
} |
|
||||||
|
|
||||||
func fromRSAPublicKey(cryptoPublicKey *rsa.PublicKey) *rsaPublicKey { |
|
||||||
return &rsaPublicKey{cryptoPublicKey, map[string]interface{}{}} |
|
||||||
} |
|
||||||
|
|
||||||
// KeyType returns the JWK key type for RSA keys, i.e., "RSA".
|
|
||||||
func (k *rsaPublicKey) KeyType() string { |
|
||||||
return "RSA" |
|
||||||
} |
|
||||||
|
|
||||||
// KeyID returns a distinct identifier which is unique to this Public Key.
|
|
||||||
func (k *rsaPublicKey) KeyID() string { |
|
||||||
return keyIDFromCryptoKey(k) |
|
||||||
} |
|
||||||
|
|
||||||
func (k *rsaPublicKey) String() string { |
|
||||||
return fmt.Sprintf("RSA Public Key <%s>", k.KeyID()) |
|
||||||
} |
|
||||||
|
|
||||||
// Verify verifyies the signature of the data in the io.Reader using this Public Key.
|
|
||||||
// The alg parameter should be the name of the JWA digital signature algorithm
|
|
||||||
// which was used to produce the signature and should be supported by this
|
|
||||||
// public key. Returns a nil error if the signature is valid.
|
|
||||||
func (k *rsaPublicKey) Verify(data io.Reader, alg string, signature []byte) error { |
|
||||||
// Verify the signature of the given date, return non-nil error if valid.
|
|
||||||
sigAlg, err := rsaSignatureAlgorithmByName(alg) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to verify Signature: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
hasher := sigAlg.HashID().New() |
|
||||||
_, err = io.Copy(hasher, data) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("error reading data to sign: %s", err) |
|
||||||
} |
|
||||||
hash := hasher.Sum(nil) |
|
||||||
|
|
||||||
err = rsa.VerifyPKCS1v15(k.PublicKey, sigAlg.HashID(), hash, signature) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("invalid %s signature: %s", sigAlg.HeaderParam(), err) |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// CryptoPublicKey returns the internal object which can be used as a
|
|
||||||
// crypto.PublicKey for use with other standard library operations. The type
|
|
||||||
// is either *rsa.PublicKey or *ecdsa.PublicKey
|
|
||||||
func (k *rsaPublicKey) CryptoPublicKey() crypto.PublicKey { |
|
||||||
return k.PublicKey |
|
||||||
} |
|
||||||
|
|
||||||
func (k *rsaPublicKey) toMap() map[string]interface{} { |
|
||||||
jwk := make(map[string]interface{}) |
|
||||||
for k, v := range k.extended { |
|
||||||
jwk[k] = v |
|
||||||
} |
|
||||||
jwk["kty"] = k.KeyType() |
|
||||||
jwk["kid"] = k.KeyID() |
|
||||||
jwk["n"] = joseBase64UrlEncode(k.N.Bytes()) |
|
||||||
jwk["e"] = joseBase64UrlEncode(serializeRSAPublicExponentParam(k.E)) |
|
||||||
|
|
||||||
return jwk |
|
||||||
} |
|
||||||
|
|
||||||
// MarshalJSON serializes this Public Key using the JWK JSON serialization format for
|
|
||||||
// RSA keys.
|
|
||||||
func (k *rsaPublicKey) MarshalJSON() (data []byte, err error) { |
|
||||||
return json.Marshal(k.toMap()) |
|
||||||
} |
|
||||||
|
|
||||||
// PEMBlock serializes this Public Key to DER-encoded PKIX format.
|
|
||||||
func (k *rsaPublicKey) PEMBlock() (*pem.Block, error) { |
|
||||||
derBytes, err := x509.MarshalPKIXPublicKey(k.PublicKey) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("unable to serialize RSA PublicKey to DER-encoded PKIX format: %s", err) |
|
||||||
} |
|
||||||
k.extended["kid"] = k.KeyID() // For display purposes.
|
|
||||||
return createPemBlock("PUBLIC KEY", derBytes, k.extended) |
|
||||||
} |
|
||||||
|
|
||||||
func (k *rsaPublicKey) AddExtendedField(field string, value interface{}) { |
|
||||||
k.extended[field] = value |
|
||||||
} |
|
||||||
|
|
||||||
func (k *rsaPublicKey) GetExtendedField(field string) interface{} { |
|
||||||
v, ok := k.extended[field] |
|
||||||
if !ok { |
|
||||||
return nil |
|
||||||
} |
|
||||||
return v |
|
||||||
} |
|
||||||
|
|
||||||
func rsaPublicKeyFromMap(jwk map[string]interface{}) (*rsaPublicKey, error) { |
|
||||||
// JWK key type (kty) has already been determined to be "RSA".
|
|
||||||
// Need to extract 'n', 'e', and 'kid' and check for
|
|
||||||
// consistency.
|
|
||||||
|
|
||||||
// Get the modulus parameter N.
|
|
||||||
nB64Url, err := stringFromMap(jwk, "n") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK RSA Public Key modulus: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
n, err := parseRSAModulusParam(nB64Url) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK RSA Public Key modulus: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
// Get the public exponent E.
|
|
||||||
eB64Url, err := stringFromMap(jwk, "e") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK RSA Public Key exponent: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
e, err := parseRSAPublicExponentParam(eB64Url) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK RSA Public Key exponent: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
key := &rsaPublicKey{ |
|
||||||
PublicKey: &rsa.PublicKey{N: n, E: e}, |
|
||||||
} |
|
||||||
|
|
||||||
// Key ID is optional, but if it exists, it should match the key.
|
|
||||||
_, ok := jwk["kid"] |
|
||||||
if ok { |
|
||||||
kid, err := stringFromMap(jwk, "kid") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK RSA Public Key ID: %s", err) |
|
||||||
} |
|
||||||
if kid != key.KeyID() { |
|
||||||
return nil, fmt.Errorf("JWK RSA Public Key ID does not match: %s", kid) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if _, ok := jwk["d"]; ok { |
|
||||||
return nil, fmt.Errorf("JWK RSA Public Key cannot contain private exponent") |
|
||||||
} |
|
||||||
|
|
||||||
key.extended = jwk |
|
||||||
|
|
||||||
return key, nil |
|
||||||
} |
|
||||||
|
|
||||||
/* |
|
||||||
* RSA DSA PRIVATE KEY |
|
||||||
*/ |
|
||||||
|
|
||||||
// rsaPrivateKey implements a JWK Private Key using RSA digital signature algorithms.
|
|
||||||
type rsaPrivateKey struct { |
|
||||||
rsaPublicKey |
|
||||||
*rsa.PrivateKey |
|
||||||
} |
|
||||||
|
|
||||||
func fromRSAPrivateKey(cryptoPrivateKey *rsa.PrivateKey) *rsaPrivateKey { |
|
||||||
return &rsaPrivateKey{ |
|
||||||
*fromRSAPublicKey(&cryptoPrivateKey.PublicKey), |
|
||||||
cryptoPrivateKey, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// PublicKey returns the Public Key data associated with this Private Key.
|
|
||||||
func (k *rsaPrivateKey) PublicKey() PublicKey { |
|
||||||
return &k.rsaPublicKey |
|
||||||
} |
|
||||||
|
|
||||||
func (k *rsaPrivateKey) String() string { |
|
||||||
return fmt.Sprintf("RSA Private Key <%s>", k.KeyID()) |
|
||||||
} |
|
||||||
|
|
||||||
// Sign signs the data read from the io.Reader using a signature algorithm supported
|
|
||||||
// by the RSA private key. If the specified hashing algorithm is supported by
|
|
||||||
// this key, that hash function is used to generate the signature otherwise the
|
|
||||||
// the default hashing algorithm for this key is used. Returns the signature
|
|
||||||
// and the name of the JWK signature algorithm used, e.g., "RS256", "RS384",
|
|
||||||
// "RS512".
|
|
||||||
func (k *rsaPrivateKey) Sign(data io.Reader, hashID crypto.Hash) (signature []byte, alg string, err error) { |
|
||||||
// Generate a signature of the data using the internal alg.
|
|
||||||
sigAlg := rsaPKCS1v15SignatureAlgorithmForHashID(hashID) |
|
||||||
hasher := sigAlg.HashID().New() |
|
||||||
|
|
||||||
_, err = io.Copy(hasher, data) |
|
||||||
if err != nil { |
|
||||||
return nil, "", fmt.Errorf("error reading data to sign: %s", err) |
|
||||||
} |
|
||||||
hash := hasher.Sum(nil) |
|
||||||
|
|
||||||
signature, err = rsa.SignPKCS1v15(rand.Reader, k.PrivateKey, sigAlg.HashID(), hash) |
|
||||||
if err != nil { |
|
||||||
return nil, "", fmt.Errorf("error producing signature: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
alg = sigAlg.HeaderParam() |
|
||||||
|
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// CryptoPrivateKey returns the internal object which can be used as a
|
|
||||||
// crypto.PublicKey for use with other standard library operations. The type
|
|
||||||
// is either *rsa.PublicKey or *ecdsa.PublicKey
|
|
||||||
func (k *rsaPrivateKey) CryptoPrivateKey() crypto.PrivateKey { |
|
||||||
return k.PrivateKey |
|
||||||
} |
|
||||||
|
|
||||||
func (k *rsaPrivateKey) toMap() map[string]interface{} { |
|
||||||
k.Precompute() // Make sure the precomputed values are stored.
|
|
||||||
jwk := k.rsaPublicKey.toMap() |
|
||||||
|
|
||||||
jwk["d"] = joseBase64UrlEncode(k.D.Bytes()) |
|
||||||
jwk["p"] = joseBase64UrlEncode(k.Primes[0].Bytes()) |
|
||||||
jwk["q"] = joseBase64UrlEncode(k.Primes[1].Bytes()) |
|
||||||
jwk["dp"] = joseBase64UrlEncode(k.Precomputed.Dp.Bytes()) |
|
||||||
jwk["dq"] = joseBase64UrlEncode(k.Precomputed.Dq.Bytes()) |
|
||||||
jwk["qi"] = joseBase64UrlEncode(k.Precomputed.Qinv.Bytes()) |
|
||||||
|
|
||||||
otherPrimes := k.Primes[2:] |
|
||||||
|
|
||||||
if len(otherPrimes) > 0 { |
|
||||||
otherPrimesInfo := make([]interface{}, len(otherPrimes)) |
|
||||||
for i, r := range otherPrimes { |
|
||||||
otherPrimeInfo := make(map[string]string, 3) |
|
||||||
otherPrimeInfo["r"] = joseBase64UrlEncode(r.Bytes()) |
|
||||||
crtVal := k.Precomputed.CRTValues[i] |
|
||||||
otherPrimeInfo["d"] = joseBase64UrlEncode(crtVal.Exp.Bytes()) |
|
||||||
otherPrimeInfo["t"] = joseBase64UrlEncode(crtVal.Coeff.Bytes()) |
|
||||||
otherPrimesInfo[i] = otherPrimeInfo |
|
||||||
} |
|
||||||
jwk["oth"] = otherPrimesInfo |
|
||||||
} |
|
||||||
|
|
||||||
return jwk |
|
||||||
} |
|
||||||
|
|
||||||
// MarshalJSON serializes this Private Key using the JWK JSON serialization format for
|
|
||||||
// RSA keys.
|
|
||||||
func (k *rsaPrivateKey) MarshalJSON() (data []byte, err error) { |
|
||||||
return json.Marshal(k.toMap()) |
|
||||||
} |
|
||||||
|
|
||||||
// PEMBlock serializes this Private Key to DER-encoded PKIX format.
|
|
||||||
func (k *rsaPrivateKey) PEMBlock() (*pem.Block, error) { |
|
||||||
derBytes := x509.MarshalPKCS1PrivateKey(k.PrivateKey) |
|
||||||
k.extended["keyID"] = k.KeyID() // For display purposes.
|
|
||||||
return createPemBlock("RSA PRIVATE KEY", derBytes, k.extended) |
|
||||||
} |
|
||||||
|
|
||||||
func rsaPrivateKeyFromMap(jwk map[string]interface{}) (*rsaPrivateKey, error) { |
|
||||||
// The JWA spec for RSA Private Keys (draft rfc section 5.3.2) states that
|
|
||||||
// only the private key exponent 'd' is REQUIRED, the others are just for
|
|
||||||
// signature/decryption optimizations and SHOULD be included when the JWK
|
|
||||||
// is produced. We MAY choose to accept a JWK which only includes 'd', but
|
|
||||||
// we're going to go ahead and not choose to accept it without the extra
|
|
||||||
// fields. Only the 'oth' field will be optional (for multi-prime keys).
|
|
||||||
privateExponent, err := parseRSAPrivateKeyParamFromMap(jwk, "d") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK RSA Private Key exponent: %s", err) |
|
||||||
} |
|
||||||
firstPrimeFactor, err := parseRSAPrivateKeyParamFromMap(jwk, "p") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK RSA Private Key prime factor: %s", err) |
|
||||||
} |
|
||||||
secondPrimeFactor, err := parseRSAPrivateKeyParamFromMap(jwk, "q") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK RSA Private Key prime factor: %s", err) |
|
||||||
} |
|
||||||
firstFactorCRT, err := parseRSAPrivateKeyParamFromMap(jwk, "dp") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK RSA Private Key CRT exponent: %s", err) |
|
||||||
} |
|
||||||
secondFactorCRT, err := parseRSAPrivateKeyParamFromMap(jwk, "dq") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK RSA Private Key CRT exponent: %s", err) |
|
||||||
} |
|
||||||
crtCoeff, err := parseRSAPrivateKeyParamFromMap(jwk, "qi") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK RSA Private Key CRT coefficient: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
var oth interface{} |
|
||||||
if _, ok := jwk["oth"]; ok { |
|
||||||
oth = jwk["oth"] |
|
||||||
delete(jwk, "oth") |
|
||||||
} |
|
||||||
|
|
||||||
// JWK key type (kty) has already been determined to be "RSA".
|
|
||||||
// Need to extract the public key information, then extract the private
|
|
||||||
// key values.
|
|
||||||
publicKey, err := rsaPublicKeyFromMap(jwk) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
privateKey := &rsa.PrivateKey{ |
|
||||||
PublicKey: *publicKey.PublicKey, |
|
||||||
D: privateExponent, |
|
||||||
Primes: []*big.Int{firstPrimeFactor, secondPrimeFactor}, |
|
||||||
Precomputed: rsa.PrecomputedValues{ |
|
||||||
Dp: firstFactorCRT, |
|
||||||
Dq: secondFactorCRT, |
|
||||||
Qinv: crtCoeff, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
if oth != nil { |
|
||||||
// Should be an array of more JSON objects.
|
|
||||||
otherPrimesInfo, ok := oth.([]interface{}) |
|
||||||
if !ok { |
|
||||||
return nil, errors.New("JWK RSA Private Key: Invalid other primes info: must be an array") |
|
||||||
} |
|
||||||
numOtherPrimeFactors := len(otherPrimesInfo) |
|
||||||
if numOtherPrimeFactors == 0 { |
|
||||||
return nil, errors.New("JWK RSA Privake Key: Invalid other primes info: must be absent or non-empty") |
|
||||||
} |
|
||||||
otherPrimeFactors := make([]*big.Int, numOtherPrimeFactors) |
|
||||||
productOfPrimes := new(big.Int).Mul(firstPrimeFactor, secondPrimeFactor) |
|
||||||
crtValues := make([]rsa.CRTValue, numOtherPrimeFactors) |
|
||||||
|
|
||||||
for i, val := range otherPrimesInfo { |
|
||||||
otherPrimeinfo, ok := val.(map[string]interface{}) |
|
||||||
if !ok { |
|
||||||
return nil, errors.New("JWK RSA Private Key: Invalid other prime info: must be a JSON object") |
|
||||||
} |
|
||||||
|
|
||||||
otherPrimeFactor, err := parseRSAPrivateKeyParamFromMap(otherPrimeinfo, "r") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK RSA Private Key prime factor: %s", err) |
|
||||||
} |
|
||||||
otherFactorCRT, err := parseRSAPrivateKeyParamFromMap(otherPrimeinfo, "d") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK RSA Private Key CRT exponent: %s", err) |
|
||||||
} |
|
||||||
otherCrtCoeff, err := parseRSAPrivateKeyParamFromMap(otherPrimeinfo, "t") |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("JWK RSA Private Key CRT coefficient: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
crtValue := crtValues[i] |
|
||||||
crtValue.Exp = otherFactorCRT |
|
||||||
crtValue.Coeff = otherCrtCoeff |
|
||||||
crtValue.R = productOfPrimes |
|
||||||
otherPrimeFactors[i] = otherPrimeFactor |
|
||||||
productOfPrimes = new(big.Int).Mul(productOfPrimes, otherPrimeFactor) |
|
||||||
} |
|
||||||
|
|
||||||
privateKey.Primes = append(privateKey.Primes, otherPrimeFactors...) |
|
||||||
privateKey.Precomputed.CRTValues = crtValues |
|
||||||
} |
|
||||||
|
|
||||||
key := &rsaPrivateKey{ |
|
||||||
rsaPublicKey: *publicKey, |
|
||||||
PrivateKey: privateKey, |
|
||||||
} |
|
||||||
|
|
||||||
return key, nil |
|
||||||
} |
|
||||||
|
|
||||||
/* |
|
||||||
* Key Generation Functions. |
|
||||||
*/ |
|
||||||
|
|
||||||
func generateRSAPrivateKey(bits int) (k *rsaPrivateKey, err error) { |
|
||||||
k = new(rsaPrivateKey) |
|
||||||
k.PrivateKey, err = rsa.GenerateKey(rand.Reader, bits) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
k.rsaPublicKey.PublicKey = &k.PrivateKey.PublicKey |
|
||||||
k.extended = make(map[string]interface{}) |
|
||||||
|
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// GenerateRSA2048PrivateKey generates a key pair using 2048-bit RSA.
|
|
||||||
func GenerateRSA2048PrivateKey() (PrivateKey, error) { |
|
||||||
k, err := generateRSAPrivateKey(2048) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("error generating RSA 2048-bit key: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
return k, nil |
|
||||||
} |
|
||||||
|
|
||||||
// GenerateRSA3072PrivateKey generates a key pair using 3072-bit RSA.
|
|
||||||
func GenerateRSA3072PrivateKey() (PrivateKey, error) { |
|
||||||
k, err := generateRSAPrivateKey(3072) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("error generating RSA 3072-bit key: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
return k, nil |
|
||||||
} |
|
||||||
|
|
||||||
// GenerateRSA4096PrivateKey generates a key pair using 4096-bit RSA.
|
|
||||||
func GenerateRSA4096PrivateKey() (PrivateKey, error) { |
|
||||||
k, err := generateRSAPrivateKey(4096) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("error generating RSA 4096-bit key: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
return k, nil |
|
||||||
} |
|
@ -1,363 +0,0 @@ |
|||||||
package libtrust |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"crypto" |
|
||||||
"crypto/elliptic" |
|
||||||
"crypto/tls" |
|
||||||
"crypto/x509" |
|
||||||
"encoding/base32" |
|
||||||
"encoding/base64" |
|
||||||
"encoding/binary" |
|
||||||
"encoding/pem" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"math/big" |
|
||||||
"net/url" |
|
||||||
"os" |
|
||||||
"path/filepath" |
|
||||||
"strings" |
|
||||||
"time" |
|
||||||
) |
|
||||||
|
|
||||||
// LoadOrCreateTrustKey will load a PrivateKey from the specified path
|
|
||||||
func LoadOrCreateTrustKey(trustKeyPath string) (PrivateKey, error) { |
|
||||||
if err := os.MkdirAll(filepath.Dir(trustKeyPath), 0700); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
trustKey, err := LoadKeyFile(trustKeyPath) |
|
||||||
if err == ErrKeyFileDoesNotExist { |
|
||||||
trustKey, err = GenerateECP256PrivateKey() |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("error generating key: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
if err := SaveKey(trustKeyPath, trustKey); err != nil { |
|
||||||
return nil, fmt.Errorf("error saving key file: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
dir, file := filepath.Split(trustKeyPath) |
|
||||||
if err := SavePublicKey(filepath.Join(dir, "public-"+file), trustKey.PublicKey()); err != nil { |
|
||||||
return nil, fmt.Errorf("error saving public key file: %s", err) |
|
||||||
} |
|
||||||
} else if err != nil { |
|
||||||
return nil, fmt.Errorf("error loading key file: %s", err) |
|
||||||
} |
|
||||||
return trustKey, nil |
|
||||||
} |
|
||||||
|
|
||||||
// NewIdentityAuthTLSClientConfig returns a tls.Config configured to use identity
|
|
||||||
// based authentication from the specified dockerUrl, the rootConfigPath and
|
|
||||||
// the server name to which it is connecting.
|
|
||||||
// If trustUnknownHosts is true it will automatically add the host to the
|
|
||||||
// known-hosts.json in rootConfigPath.
|
|
||||||
func NewIdentityAuthTLSClientConfig(dockerUrl string, trustUnknownHosts bool, rootConfigPath string, serverName string) (*tls.Config, error) { |
|
||||||
tlsConfig := newTLSConfig() |
|
||||||
|
|
||||||
trustKeyPath := filepath.Join(rootConfigPath, "key.json") |
|
||||||
knownHostsPath := filepath.Join(rootConfigPath, "known-hosts.json") |
|
||||||
|
|
||||||
u, err := url.Parse(dockerUrl) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("unable to parse machine url") |
|
||||||
} |
|
||||||
|
|
||||||
if u.Scheme == "unix" { |
|
||||||
return nil, nil |
|
||||||
} |
|
||||||
|
|
||||||
addr := u.Host |
|
||||||
proto := "tcp" |
|
||||||
|
|
||||||
trustKey, err := LoadOrCreateTrustKey(trustKeyPath) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("unable to load trust key: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
knownHosts, err := LoadKeySetFile(knownHostsPath) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("could not load trusted hosts file: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
allowedHosts, err := FilterByHosts(knownHosts, addr, false) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("error filtering hosts: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
certPool, err := GenerateCACertPool(trustKey, allowedHosts) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("Could not create CA pool: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
tlsConfig.ServerName = serverName |
|
||||||
tlsConfig.RootCAs = certPool |
|
||||||
|
|
||||||
x509Cert, err := GenerateSelfSignedClientCert(trustKey) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("certificate generation error: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
tlsConfig.Certificates = []tls.Certificate{{ |
|
||||||
Certificate: [][]byte{x509Cert.Raw}, |
|
||||||
PrivateKey: trustKey.CryptoPrivateKey(), |
|
||||||
Leaf: x509Cert, |
|
||||||
}} |
|
||||||
|
|
||||||
tlsConfig.InsecureSkipVerify = true |
|
||||||
|
|
||||||
testConn, err := tls.Dial(proto, addr, tlsConfig) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("tls Handshake error: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
opts := x509.VerifyOptions{ |
|
||||||
Roots: tlsConfig.RootCAs, |
|
||||||
CurrentTime: time.Now(), |
|
||||||
DNSName: tlsConfig.ServerName, |
|
||||||
Intermediates: x509.NewCertPool(), |
|
||||||
} |
|
||||||
|
|
||||||
certs := testConn.ConnectionState().PeerCertificates |
|
||||||
for i, cert := range certs { |
|
||||||
if i == 0 { |
|
||||||
continue |
|
||||||
} |
|
||||||
opts.Intermediates.AddCert(cert) |
|
||||||
} |
|
||||||
|
|
||||||
if _, err := certs[0].Verify(opts); err != nil { |
|
||||||
if _, ok := err.(x509.UnknownAuthorityError); ok { |
|
||||||
if trustUnknownHosts { |
|
||||||
pubKey, err := FromCryptoPublicKey(certs[0].PublicKey) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("error extracting public key from cert: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
pubKey.AddExtendedField("hosts", []string{addr}) |
|
||||||
|
|
||||||
if err := AddKeySetFile(knownHostsPath, pubKey); err != nil { |
|
||||||
return nil, fmt.Errorf("error adding machine to known hosts: %s", err) |
|
||||||
} |
|
||||||
} else { |
|
||||||
return nil, fmt.Errorf("unable to connect. unknown host: %s", addr) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
testConn.Close() |
|
||||||
tlsConfig.InsecureSkipVerify = false |
|
||||||
|
|
||||||
return tlsConfig, nil |
|
||||||
} |
|
||||||
|
|
||||||
// joseBase64UrlEncode encodes the given data using the standard base64 url
|
|
||||||
// encoding format but with all trailing '=' characters omitted in accordance
|
|
||||||
// with the jose specification.
|
|
||||||
// http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-2
|
|
||||||
func joseBase64UrlEncode(b []byte) string { |
|
||||||
return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=") |
|
||||||
} |
|
||||||
|
|
||||||
// joseBase64UrlDecode decodes the given string using the standard base64 url
|
|
||||||
// decoder but first adds the appropriate number of trailing '=' characters in
|
|
||||||
// accordance with the jose specification.
|
|
||||||
// http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-2
|
|
||||||
func joseBase64UrlDecode(s string) ([]byte, error) { |
|
||||||
s = strings.Replace(s, "\n", "", -1) |
|
||||||
s = strings.Replace(s, " ", "", -1) |
|
||||||
switch len(s) % 4 { |
|
||||||
case 0: |
|
||||||
case 2: |
|
||||||
s += "==" |
|
||||||
case 3: |
|
||||||
s += "=" |
|
||||||
default: |
|
||||||
return nil, errors.New("illegal base64url string") |
|
||||||
} |
|
||||||
return base64.URLEncoding.DecodeString(s) |
|
||||||
} |
|
||||||
|
|
||||||
func keyIDEncode(b []byte) string { |
|
||||||
s := strings.TrimRight(base32.StdEncoding.EncodeToString(b), "=") |
|
||||||
var buf bytes.Buffer |
|
||||||
var i int |
|
||||||
for i = 0; i < len(s)/4-1; i++ { |
|
||||||
start := i * 4 |
|
||||||
end := start + 4 |
|
||||||
buf.WriteString(s[start:end] + ":") |
|
||||||
} |
|
||||||
buf.WriteString(s[i*4:]) |
|
||||||
return buf.String() |
|
||||||
} |
|
||||||
|
|
||||||
func keyIDFromCryptoKey(pubKey PublicKey) string { |
|
||||||
// Generate and return a 'libtrust' fingerprint of the public key.
|
|
||||||
// For an RSA key this should be:
|
|
||||||
// SHA256(DER encoded ASN1)
|
|
||||||
// Then truncated to 240 bits and encoded into 12 base32 groups like so:
|
|
||||||
// ABCD:EFGH:IJKL:MNOP:QRST:UVWX:YZ23:4567:ABCD:EFGH:IJKL:MNOP
|
|
||||||
derBytes, err := x509.MarshalPKIXPublicKey(pubKey.CryptoPublicKey()) |
|
||||||
if err != nil { |
|
||||||
return "" |
|
||||||
} |
|
||||||
hasher := crypto.SHA256.New() |
|
||||||
hasher.Write(derBytes) |
|
||||||
return keyIDEncode(hasher.Sum(nil)[:30]) |
|
||||||
} |
|
||||||
|
|
||||||
func stringFromMap(m map[string]interface{}, key string) (string, error) { |
|
||||||
val, ok := m[key] |
|
||||||
if !ok { |
|
||||||
return "", fmt.Errorf("%q value not specified", key) |
|
||||||
} |
|
||||||
|
|
||||||
str, ok := val.(string) |
|
||||||
if !ok { |
|
||||||
return "", fmt.Errorf("%q value must be a string", key) |
|
||||||
} |
|
||||||
delete(m, key) |
|
||||||
|
|
||||||
return str, nil |
|
||||||
} |
|
||||||
|
|
||||||
func parseECCoordinate(cB64Url string, curve elliptic.Curve) (*big.Int, error) { |
|
||||||
curveByteLen := (curve.Params().BitSize + 7) >> 3 |
|
||||||
|
|
||||||
cBytes, err := joseBase64UrlDecode(cB64Url) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("invalid base64 URL encoding: %s", err) |
|
||||||
} |
|
||||||
cByteLength := len(cBytes) |
|
||||||
if cByteLength != curveByteLen { |
|
||||||
return nil, fmt.Errorf("invalid number of octets: got %d, should be %d", cByteLength, curveByteLen) |
|
||||||
} |
|
||||||
return new(big.Int).SetBytes(cBytes), nil |
|
||||||
} |
|
||||||
|
|
||||||
func parseECPrivateParam(dB64Url string, curve elliptic.Curve) (*big.Int, error) { |
|
||||||
dBytes, err := joseBase64UrlDecode(dB64Url) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("invalid base64 URL encoding: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
// The length of this octet string MUST be ceiling(log-base-2(n)/8)
|
|
||||||
// octets (where n is the order of the curve). This is because the private
|
|
||||||
// key d must be in the interval [1, n-1] so the bitlength of d should be
|
|
||||||
// no larger than the bitlength of n-1. The easiest way to find the octet
|
|
||||||
// length is to take bitlength(n-1), add 7 to force a carry, and shift this
|
|
||||||
// bit sequence right by 3, which is essentially dividing by 8 and adding
|
|
||||||
// 1 if there is any remainder. Thus, the private key value d should be
|
|
||||||
// output to (bitlength(n-1)+7)>>3 octets.
|
|
||||||
n := curve.Params().N |
|
||||||
octetLength := (new(big.Int).Sub(n, big.NewInt(1)).BitLen() + 7) >> 3 |
|
||||||
dByteLength := len(dBytes) |
|
||||||
|
|
||||||
if dByteLength != octetLength { |
|
||||||
return nil, fmt.Errorf("invalid number of octets: got %d, should be %d", dByteLength, octetLength) |
|
||||||
} |
|
||||||
|
|
||||||
return new(big.Int).SetBytes(dBytes), nil |
|
||||||
} |
|
||||||
|
|
||||||
func parseRSAModulusParam(nB64Url string) (*big.Int, error) { |
|
||||||
nBytes, err := joseBase64UrlDecode(nB64Url) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("invalid base64 URL encoding: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
return new(big.Int).SetBytes(nBytes), nil |
|
||||||
} |
|
||||||
|
|
||||||
func serializeRSAPublicExponentParam(e int) []byte { |
|
||||||
// We MUST use the minimum number of octets to represent E.
|
|
||||||
// E is supposed to be 65537 for performance and security reasons
|
|
||||||
// and is what golang's rsa package generates, but it might be
|
|
||||||
// different if imported from some other generator.
|
|
||||||
buf := make([]byte, 4) |
|
||||||
binary.BigEndian.PutUint32(buf, uint32(e)) |
|
||||||
var i int |
|
||||||
for i = 0; i < 8; i++ { |
|
||||||
if buf[i] != 0 { |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
return buf[i:] |
|
||||||
} |
|
||||||
|
|
||||||
func parseRSAPublicExponentParam(eB64Url string) (int, error) { |
|
||||||
eBytes, err := joseBase64UrlDecode(eB64Url) |
|
||||||
if err != nil { |
|
||||||
return 0, fmt.Errorf("invalid base64 URL encoding: %s", err) |
|
||||||
} |
|
||||||
// Only the minimum number of bytes were used to represent E, but
|
|
||||||
// binary.BigEndian.Uint32 expects at least 4 bytes, so we need
|
|
||||||
// to add zero padding if necassary.
|
|
||||||
byteLen := len(eBytes) |
|
||||||
buf := make([]byte, 4-byteLen, 4) |
|
||||||
eBytes = append(buf, eBytes...) |
|
||||||
|
|
||||||
return int(binary.BigEndian.Uint32(eBytes)), nil |
|
||||||
} |
|
||||||
|
|
||||||
func parseRSAPrivateKeyParamFromMap(m map[string]interface{}, key string) (*big.Int, error) { |
|
||||||
b64Url, err := stringFromMap(m, key) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
paramBytes, err := joseBase64UrlDecode(b64Url) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("invaled base64 URL encoding: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
return new(big.Int).SetBytes(paramBytes), nil |
|
||||||
} |
|
||||||
|
|
||||||
func createPemBlock(name string, derBytes []byte, headers map[string]interface{}) (*pem.Block, error) { |
|
||||||
pemBlock := &pem.Block{Type: name, Bytes: derBytes, Headers: map[string]string{}} |
|
||||||
for k, v := range headers { |
|
||||||
switch val := v.(type) { |
|
||||||
case string: |
|
||||||
pemBlock.Headers[k] = val |
|
||||||
case []string: |
|
||||||
if k == "hosts" { |
|
||||||
pemBlock.Headers[k] = strings.Join(val, ",") |
|
||||||
} else { |
|
||||||
// Return error, non-encodable type
|
|
||||||
} |
|
||||||
default: |
|
||||||
// Return error, non-encodable type
|
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return pemBlock, nil |
|
||||||
} |
|
||||||
|
|
||||||
func pubKeyFromPEMBlock(pemBlock *pem.Block) (PublicKey, error) { |
|
||||||
cryptoPublicKey, err := x509.ParsePKIXPublicKey(pemBlock.Bytes) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("unable to decode Public Key PEM data: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
pubKey, err := FromCryptoPublicKey(cryptoPublicKey) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
addPEMHeadersToKey(pemBlock, pubKey) |
|
||||||
|
|
||||||
return pubKey, nil |
|
||||||
} |
|
||||||
|
|
||||||
func addPEMHeadersToKey(pemBlock *pem.Block, pubKey PublicKey) { |
|
||||||
for key, value := range pemBlock.Headers { |
|
||||||
var safeVal interface{} |
|
||||||
if key == "hosts" { |
|
||||||
safeVal = strings.Split(value, ",") |
|
||||||
} else { |
|
||||||
safeVal = value |
|
||||||
} |
|
||||||
pubKey.AddExtendedField(key, safeVal) |
|
||||||
} |
|
||||||
} |
|
@ -1,26 +0,0 @@ |
|||||||
Copyright (c) 2015, Salesforce.com, Inc. All rights reserved. |
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without |
|
||||||
modification, are permitted provided that the following conditions are met: |
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright notice, this |
|
||||||
list of conditions and the following disclaimer. |
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above copyright notice, |
|
||||||
this list of conditions and the following disclaimer in the documentation |
|
||||||
and/or other materials provided with the distribution. |
|
||||||
|
|
||||||
* Neither the name of Salesforce.com nor the names of its contributors may be |
|
||||||
used to endorse or promote products derived from this software without |
|
||||||
specific prior written permission. |
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND |
|
||||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
||||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
|
||||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
|
||||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
|
||||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
|
||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
@ -1,151 +0,0 @@ |
|||||||
package registry |
|
||||||
|
|
||||||
import ( |
|
||||||
"net/http" |
|
||||||
"strings" |
|
||||||
) |
|
||||||
|
|
||||||
// Octet types from RFC 2616.
|
|
||||||
type octetType byte |
|
||||||
|
|
||||||
// AuthorizationChallenge carries information
|
|
||||||
// from a WWW-Authenticate response header.
|
|
||||||
type AuthorizationChallenge struct { |
|
||||||
Scheme string |
|
||||||
Parameters map[string]string |
|
||||||
} |
|
||||||
|
|
||||||
var octetTypes [256]octetType |
|
||||||
|
|
||||||
const ( |
|
||||||
isToken octetType = 1 << iota |
|
||||||
isSpace |
|
||||||
) |
|
||||||
|
|
||||||
//nolint:gochecknoinits
|
|
||||||
func init() { |
|
||||||
// OCTET = <any 8-bit sequence of data>
|
|
||||||
// CHAR = <any US-ASCII character (octets 0 - 127)>
|
|
||||||
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
|
||||||
// CR = <US-ASCII CR, carriage return (13)>
|
|
||||||
// LF = <US-ASCII LF, linefeed (10)>
|
|
||||||
// SP = <US-ASCII SP, space (32)>
|
|
||||||
// HT = <US-ASCII HT, horizontal-tab (9)>
|
|
||||||
// <"> = <US-ASCII double-quote mark (34)>
|
|
||||||
// CRLF = CR LF
|
|
||||||
// LWS = [CRLF] 1*( SP | HT )
|
|
||||||
// TEXT = <any OCTET except CTLs, but including LWS>
|
|
||||||
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
|
|
||||||
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
|
|
||||||
// token = 1*<any CHAR except CTLs or separators>
|
|
||||||
// qdtext = <any TEXT except <">>
|
|
||||||
|
|
||||||
for c := 0; c < 256; c++ { |
|
||||||
var t octetType |
|
||||||
isCtl := c <= 31 || c == 127 |
|
||||||
isChar := 0 <= c && c <= 127 |
|
||||||
isSeparator := strings.ContainsRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) |
|
||||||
if strings.ContainsRune(" \t\r\n", rune(c)) { |
|
||||||
t |= isSpace |
|
||||||
} |
|
||||||
if isChar && !isCtl && !isSeparator { |
|
||||||
t |= isToken |
|
||||||
} |
|
||||||
octetTypes[c] = t |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func parseAuthHeader(header http.Header) []*AuthorizationChallenge { |
|
||||||
var challenges []*AuthorizationChallenge |
|
||||||
for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] { |
|
||||||
v, p := parseValueAndParams(h) |
|
||||||
if v != "" { |
|
||||||
challenges = append(challenges, &AuthorizationChallenge{Scheme: v, Parameters: p}) |
|
||||||
} |
|
||||||
} |
|
||||||
return challenges |
|
||||||
} |
|
||||||
|
|
||||||
func parseValueAndParams(header string) (value string, params map[string]string) { |
|
||||||
params = make(map[string]string) |
|
||||||
value, s := expectToken(header) |
|
||||||
if value == "" { |
|
||||||
return |
|
||||||
} |
|
||||||
value = strings.ToLower(value) |
|
||||||
s = "," + skipSpace(s) |
|
||||||
for strings.HasPrefix(s, ",") { |
|
||||||
var pkey string |
|
||||||
pkey, s = expectToken(skipSpace(s[1:])) |
|
||||||
if pkey == "" { |
|
||||||
return |
|
||||||
} |
|
||||||
if !strings.HasPrefix(s, "=") { |
|
||||||
return |
|
||||||
} |
|
||||||
var pvalue string |
|
||||||
pvalue, s = expectTokenOrQuoted(s[1:]) |
|
||||||
if pvalue == "" { |
|
||||||
return |
|
||||||
} |
|
||||||
pkey = strings.ToLower(pkey) |
|
||||||
params[pkey] = pvalue |
|
||||||
s = skipSpace(s) |
|
||||||
} |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
func skipSpace(s string) (rest string) { |
|
||||||
i := 0 |
|
||||||
for ; i < len(s); i++ { |
|
||||||
if octetTypes[s[i]]&isSpace == 0 { |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
return s[i:] |
|
||||||
} |
|
||||||
|
|
||||||
func expectToken(s string) (token, rest string) { |
|
||||||
i := 0 |
|
||||||
for ; i < len(s); i++ { |
|
||||||
if octetTypes[s[i]]&isToken == 0 { |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
return s[:i], s[i:] |
|
||||||
} |
|
||||||
|
|
||||||
func expectTokenOrQuoted(s string) (value string, rest string) { |
|
||||||
if !strings.HasPrefix(s, "\"") { |
|
||||||
return expectToken(s) |
|
||||||
} |
|
||||||
s = s[1:] |
|
||||||
for i := 0; i < len(s); i++ { |
|
||||||
switch s[i] { |
|
||||||
case '"': |
|
||||||
return s[:i], s[i+1:] |
|
||||||
case '\\': |
|
||||||
p := make([]byte, len(s)-1) |
|
||||||
j := copy(p, s[:i]) |
|
||||||
escape := true |
|
||||||
for i += i; i < len(s); i++ { |
|
||||||
b := s[i] |
|
||||||
switch { |
|
||||||
case escape: |
|
||||||
escape = false |
|
||||||
p[j] = b |
|
||||||
j++ |
|
||||||
case b == '\\': |
|
||||||
escape = true |
|
||||||
case b == '"': |
|
||||||
return string(p[:j]), s[i+1:] |
|
||||||
default: |
|
||||||
p[j] = b |
|
||||||
j++ |
|
||||||
} |
|
||||||
} |
|
||||||
return "", "" |
|
||||||
} |
|
||||||
} |
|
||||||
return "", "" |
|
||||||
} |
|
@ -1,23 +0,0 @@ |
|||||||
package registry |
|
||||||
|
|
||||||
import ( |
|
||||||
"net/http" |
|
||||||
"strings" |
|
||||||
) |
|
||||||
|
|
||||||
type BasicTransport struct { |
|
||||||
Transport http.RoundTripper |
|
||||||
URL string |
|
||||||
Username string |
|
||||||
Password string |
|
||||||
} |
|
||||||
|
|
||||||
func (t *BasicTransport) RoundTrip(req *http.Request) (*http.Response, error) { |
|
||||||
if strings.HasPrefix(req.URL.String(), t.URL) { |
|
||||||
if t.Username != "" || t.Password != "" { |
|
||||||
req.SetBasicAuth(t.Username, t.Password) |
|
||||||
} |
|
||||||
} |
|
||||||
resp, err := t.Transport.RoundTrip(req) |
|
||||||
return resp, err |
|
||||||
} |
|
@ -1,108 +0,0 @@ |
|||||||
package registry |
|
||||||
|
|
||||||
import ( |
|
||||||
"io" |
|
||||||
"net/http" |
|
||||||
"net/url" |
|
||||||
|
|
||||||
"github.com/docker/distribution" |
|
||||||
digest "github.com/opencontainers/go-digest" |
|
||||||
) |
|
||||||
|
|
||||||
func (registry *Registry) DownloadBlob(repository string, digest digest.Digest) (io.ReadCloser, error) { |
|
||||||
url := registry.url("/v2/%s/blobs/%s", repository, digest) |
|
||||||
registry.Logf("registry.blob.download url=%s repository=%s digest=%s", url, repository, digest) |
|
||||||
|
|
||||||
resp, err := registry.Client.Get(url) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return resp.Body, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (registry *Registry) UploadBlob(repository string, digest digest.Digest, content io.Reader) error { |
|
||||||
uploadURL, err := registry.initiateUpload(repository) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
q := uploadURL.Query() |
|
||||||
q.Set("digest", digest.String()) |
|
||||||
uploadURL.RawQuery = q.Encode() |
|
||||||
|
|
||||||
registry.Logf("registry.blob.upload url=%s repository=%s digest=%s", uploadURL, repository, digest) |
|
||||||
|
|
||||||
upload, err := http.NewRequest("PUT", uploadURL.String(), content) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
upload.Header.Set("Content-Type", "application/octet-stream") |
|
||||||
|
|
||||||
_, err = registry.Client.Do(upload) |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
func (registry *Registry) HasBlob(repository string, digest digest.Digest) (bool, error) { |
|
||||||
checkURL := registry.url("/v2/%s/blobs/%s", repository, digest) |
|
||||||
registry.Logf("registry.blob.check url=%s repository=%s digest=%s", checkURL, repository, digest) |
|
||||||
|
|
||||||
resp, err := registry.Client.Head(checkURL) |
|
||||||
if resp != nil { |
|
||||||
defer resp.Body.Close() |
|
||||||
} |
|
||||||
if err == nil { |
|
||||||
return resp.StatusCode == http.StatusOK, nil |
|
||||||
} |
|
||||||
|
|
||||||
urlErr, ok := err.(*url.Error) |
|
||||||
if !ok { |
|
||||||
return false, err |
|
||||||
} |
|
||||||
httpErr, ok := urlErr.Err.(*HTTPStatusError) |
|
||||||
if !ok { |
|
||||||
return false, err |
|
||||||
} |
|
||||||
if httpErr.Response.StatusCode == http.StatusNotFound { |
|
||||||
return false, nil |
|
||||||
} |
|
||||||
|
|
||||||
return false, err |
|
||||||
} |
|
||||||
|
|
||||||
func (registry *Registry) BlobMetadata(repository string, digest digest.Digest) (distribution.Descriptor, error) { |
|
||||||
checkURL := registry.url("/v2/%s/blobs/%s", repository, digest) |
|
||||||
registry.Logf("registry.blob.check url=%s repository=%s digest=%s", checkURL, repository, digest) |
|
||||||
|
|
||||||
resp, err := registry.Client.Head(checkURL) |
|
||||||
if resp != nil { |
|
||||||
defer resp.Body.Close() |
|
||||||
} |
|
||||||
if err != nil { |
|
||||||
return distribution.Descriptor{}, err |
|
||||||
} |
|
||||||
|
|
||||||
return distribution.Descriptor{ |
|
||||||
Digest: digest, |
|
||||||
Size: resp.ContentLength, |
|
||||||
}, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (registry *Registry) initiateUpload(repository string) (*url.URL, error) { |
|
||||||
initiateURL := registry.url("/v2/%s/blobs/uploads/", repository) |
|
||||||
registry.Logf("registry.blob.initiate-upload url=%s repository=%s", initiateURL, repository) |
|
||||||
|
|
||||||
resp, err := registry.Client.Post(initiateURL, "application/octet-stream", nil) |
|
||||||
if resp != nil { |
|
||||||
defer resp.Body.Close() |
|
||||||
} |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
location := resp.Header.Get("Location") |
|
||||||
locationURL, err := url.Parse(location) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return locationURL, nil |
|
||||||
} |
|
@ -1,46 +0,0 @@ |
|||||||
package registry |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"io/ioutil" |
|
||||||
"net/http" |
|
||||||
) |
|
||||||
|
|
||||||
type HTTPStatusError struct { |
|
||||||
Response *http.Response |
|
||||||
// Copied from `Response.Body` to avoid problems with unclosed bodies later.
|
|
||||||
// Nobody calls `err.Response.Body.Close()`, ever.
|
|
||||||
Body []byte |
|
||||||
} |
|
||||||
|
|
||||||
func (err *HTTPStatusError) Error() string { |
|
||||||
return fmt.Sprintf("http: non-successful response (status=%v body=%q)", err.Response.StatusCode, err.Body) |
|
||||||
} |
|
||||||
|
|
||||||
var _ error = &HTTPStatusError{} |
|
||||||
|
|
||||||
type ErrorTransport struct { |
|
||||||
Transport http.RoundTripper |
|
||||||
} |
|
||||||
|
|
||||||
func (t *ErrorTransport) RoundTrip(request *http.Request) (*http.Response, error) { |
|
||||||
resp, err := t.Transport.RoundTrip(request) |
|
||||||
if err != nil { |
|
||||||
return resp, err |
|
||||||
} |
|
||||||
|
|
||||||
if resp.StatusCode >= 400 { |
|
||||||
defer resp.Body.Close() |
|
||||||
body, err := ioutil.ReadAll(resp.Body) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("http: failed to read response body (status=%v, err=%q)", resp.StatusCode, err) |
|
||||||
} |
|
||||||
|
|
||||||
return nil, &HTTPStatusError{ |
|
||||||
Response: resp, |
|
||||||
Body: body, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return resp, err |
|
||||||
} |
|
@ -1,50 +0,0 @@ |
|||||||
package registry |
|
||||||
|
|
||||||
import ( |
|
||||||
"encoding/json" |
|
||||||
"errors" |
|
||||||
"net/http" |
|
||||||
"regexp" |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
|
||||||
ErrNoMorePages = errors.New("no more pages") |
|
||||||
) |
|
||||||
|
|
||||||
// getPaginatedJSON accepts a string and a pointer, and returns the
|
|
||||||
// next page URL while updating pointed-to variable with a parsed JSON
|
|
||||||
// value. When there are no more pages it returns `ErrNoMorePages`.
|
|
||||||
func (registry *Registry) getPaginatedJSON(url string, response interface{}) (string, error) { |
|
||||||
resp, err := registry.Client.Get(url) |
|
||||||
if err != nil { |
|
||||||
return "", err |
|
||||||
} |
|
||||||
defer resp.Body.Close() |
|
||||||
|
|
||||||
decoder := json.NewDecoder(resp.Body) |
|
||||||
err = decoder.Decode(response) |
|
||||||
if err != nil { |
|
||||||
return "", err |
|
||||||
} |
|
||||||
return getNextLink(resp) |
|
||||||
} |
|
||||||
|
|
||||||
// Matches an RFC 5988 (https://tools.ietf.org/html/rfc5988#section-5)
|
|
||||||
// Link header. For example,
|
|
||||||
//
|
|
||||||
// <http://registry.example.com/v2/_catalog?n=5&last=tag5>; type="application/json"; rel="next"
|
|
||||||
//
|
|
||||||
// The URL is _supposed_ to be wrapped by angle brackets `< ... >`,
|
|
||||||
// but e.g., quay.io does not include them. Similarly, params like
|
|
||||||
// `rel="next"` may not have quoted values in the wild.
|
|
||||||
var nextLinkRE = regexp.MustCompile(`^ *<?([^;>]+)>? *(?:;[^;]*)*; *rel="?next"?(?:;.*)?`) |
|
||||||
|
|
||||||
func getNextLink(resp *http.Response) (string, error) { |
|
||||||
for _, link := range resp.Header[http.CanonicalHeaderKey("Link")] { |
|
||||||
parts := nextLinkRE.FindStringSubmatch(link) |
|
||||||
if parts != nil { |
|
||||||
return parts[1], nil |
|
||||||
} |
|
||||||
} |
|
||||||
return "", ErrNoMorePages |
|
||||||
} |
|
@ -1,126 +0,0 @@ |
|||||||
package registry |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"io/ioutil" |
|
||||||
"net/http" |
|
||||||
|
|
||||||
"github.com/docker/distribution" |
|
||||||
"github.com/docker/distribution/manifest/schema1" |
|
||||||
"github.com/docker/distribution/manifest/schema2" |
|
||||||
digest "github.com/opencontainers/go-digest" |
|
||||||
) |
|
||||||
|
|
||||||
func (registry *Registry) Manifest(repository, reference string) (*schema1.SignedManifest, error) { |
|
||||||
url := registry.url("/v2/%s/manifests/%s", repository, reference) |
|
||||||
registry.Logf("registry.manifest.get url=%s repository=%s reference=%s", url, repository, reference) |
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", url, nil) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
req.Header.Set("Accept", schema1.MediaTypeManifest) |
|
||||||
resp, err := registry.Client.Do(req) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
defer resp.Body.Close() |
|
||||||
body, err := ioutil.ReadAll(resp.Body) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
signedManifest := &schema1.SignedManifest{} |
|
||||||
err = signedManifest.UnmarshalJSON(body) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return signedManifest, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (registry *Registry) ManifestV2(repository, reference string) (*schema2.DeserializedManifest, error) { |
|
||||||
url := registry.url("/v2/%s/manifests/%s", repository, reference) |
|
||||||
registry.Logf("registry.manifest.get url=%s repository=%s reference=%s", url, repository, reference) |
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", url, nil) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
req.Header.Set("Accept", schema2.MediaTypeManifest) |
|
||||||
resp, err := registry.Client.Do(req) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
defer resp.Body.Close() |
|
||||||
body, err := ioutil.ReadAll(resp.Body) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
deserialized := &schema2.DeserializedManifest{} |
|
||||||
err = deserialized.UnmarshalJSON(body) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return deserialized, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (registry *Registry) ManifestDigest(repository, reference string) (digest.Digest, error) { |
|
||||||
url := registry.url("/v2/%s/manifests/%s", repository, reference) |
|
||||||
registry.Logf("registry.manifest.head url=%s repository=%s reference=%s", url, repository, reference) |
|
||||||
|
|
||||||
resp, err := registry.Client.Head(url) |
|
||||||
if resp != nil { |
|
||||||
defer resp.Body.Close() |
|
||||||
} |
|
||||||
if err != nil { |
|
||||||
return "", err |
|
||||||
} |
|
||||||
return digest.Parse(resp.Header.Get("Docker-Content-Digest")) |
|
||||||
} |
|
||||||
|
|
||||||
func (registry *Registry) DeleteManifest(repository string, digest digest.Digest) error { |
|
||||||
url := registry.url("/v2/%s/manifests/%s", repository, digest) |
|
||||||
registry.Logf("registry.manifest.delete url=%s repository=%s reference=%s", url, repository, digest) |
|
||||||
|
|
||||||
req, err := http.NewRequest("DELETE", url, nil) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
resp, err := registry.Client.Do(req) |
|
||||||
if resp != nil { |
|
||||||
defer resp.Body.Close() |
|
||||||
} |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (registry *Registry) PutManifest(repository, reference string, manifest distribution.Manifest) error { |
|
||||||
url := registry.url("/v2/%s/manifests/%s", repository, reference) |
|
||||||
registry.Logf("registry.manifest.put url=%s repository=%s reference=%s", url, repository, reference) |
|
||||||
|
|
||||||
mediaType, payload, err := manifest.Payload() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
buffer := bytes.NewBuffer(payload) |
|
||||||
req, err := http.NewRequest("PUT", url, buffer) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
req.Header.Set("Content-Type", mediaType) |
|
||||||
resp, err := registry.Client.Do(req) |
|
||||||
if resp != nil { |
|
||||||
defer resp.Body.Close() |
|
||||||
} |
|
||||||
return err |
|
||||||
} |
|
@ -1,118 +0,0 @@ |
|||||||
package registry |
|
||||||
|
|
||||||
import ( |
|
||||||
"crypto/tls" |
|
||||||
"fmt" |
|
||||||
"log" |
|
||||||
"net/http" |
|
||||||
"strings" |
|
||||||
) |
|
||||||
|
|
||||||
type LogfCallback func(format string, args ...interface{}) |
|
||||||
|
|
||||||
/* |
|
||||||
* Discard log messages silently. |
|
||||||
*/ |
|
||||||
func Quiet(format string, args ...interface{}) { |
|
||||||
/* discard logs */ |
|
||||||
} |
|
||||||
|
|
||||||
/* |
|
||||||
* Pass log messages along to Go's "log" module. |
|
||||||
*/ |
|
||||||
func Log(format string, args ...interface{}) { |
|
||||||
log.Printf(format, args...) |
|
||||||
} |
|
||||||
|
|
||||||
type Registry struct { |
|
||||||
URL string |
|
||||||
Client *http.Client |
|
||||||
Logf LogfCallback |
|
||||||
} |
|
||||||
|
|
||||||
/* |
|
||||||
* Create a new Registry with the given URL and credentials, then Ping()s it |
|
||||||
* before returning it to verify that the registry is available. |
|
||||||
* |
|
||||||
* You can, alternately, construct a Registry manually by populating the fields. |
|
||||||
* This passes http.DefaultTransport to WrapTransport when creating the |
|
||||||
* http.Client. |
|
||||||
*/ |
|
||||||
func New(registryURL, username, password string) (*Registry, error) { |
|
||||||
transport := http.DefaultTransport |
|
||||||
|
|
||||||
return newFromTransport(registryURL, username, password, transport, Log) |
|
||||||
} |
|
||||||
|
|
||||||
/* |
|
||||||
* Create a new Registry, as with New, using an http.Transport that disables |
|
||||||
* SSL certificate verification. |
|
||||||
*/ |
|
||||||
func NewInsecure(registryURL, username, password string) (*Registry, error) { |
|
||||||
transport := &http.Transport{ |
|
||||||
TLSClientConfig: &tls.Config{ |
|
||||||
// TODO: Why?
|
|
||||||
InsecureSkipVerify: true, //nolint:gosec
|
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
return newFromTransport(registryURL, username, password, transport, Log) |
|
||||||
} |
|
||||||
|
|
||||||
/* |
|
||||||
* Given an existing http.RoundTripper such as http.DefaultTransport, build the |
|
||||||
* transport stack necessary to authenticate to the Docker registry API. This |
|
||||||
* adds in support for OAuth bearer tokens and HTTP Basic auth, and sets up |
|
||||||
* error handling this library relies on. |
|
||||||
*/ |
|
||||||
func WrapTransport(transport http.RoundTripper, url, username, password string) http.RoundTripper { |
|
||||||
tokenTransport := &TokenTransport{ |
|
||||||
Transport: transport, |
|
||||||
Username: username, |
|
||||||
Password: password, |
|
||||||
} |
|
||||||
basicAuthTransport := &BasicTransport{ |
|
||||||
Transport: tokenTransport, |
|
||||||
URL: url, |
|
||||||
Username: username, |
|
||||||
Password: password, |
|
||||||
} |
|
||||||
errorTransport := &ErrorTransport{ |
|
||||||
Transport: basicAuthTransport, |
|
||||||
} |
|
||||||
return errorTransport |
|
||||||
} |
|
||||||
|
|
||||||
func newFromTransport(registryURL, username, password string, transport http.RoundTripper, logf LogfCallback) (*Registry, error) { |
|
||||||
url := strings.TrimSuffix(registryURL, "/") |
|
||||||
transport = WrapTransport(transport, url, username, password) |
|
||||||
registry := &Registry{ |
|
||||||
URL: url, |
|
||||||
Client: &http.Client{ |
|
||||||
Transport: transport, |
|
||||||
}, |
|
||||||
Logf: logf, |
|
||||||
} |
|
||||||
|
|
||||||
if err := registry.Ping(); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return registry, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (r *Registry) url(pathTemplate string, args ...interface{}) string { |
|
||||||
pathSuffix := fmt.Sprintf(pathTemplate, args...) |
|
||||||
url := fmt.Sprintf("%s%s", r.URL, pathSuffix) |
|
||||||
return url |
|
||||||
} |
|
||||||
|
|
||||||
func (r *Registry) Ping() error { |
|
||||||
url := r.url("/v2/") |
|
||||||
r.Logf("registry.ping url=%s", url) |
|
||||||
resp, err := r.Client.Get(url) |
|
||||||
if resp != nil { |
|
||||||
defer resp.Body.Close() |
|
||||||
} |
|
||||||
return err |
|
||||||
} |
|
@ -1,26 +0,0 @@ |
|||||||
package registry |
|
||||||
|
|
||||||
type repositoriesResponse struct { |
|
||||||
Repositories []string `json:"repositories"` |
|
||||||
} |
|
||||||
|
|
||||||
func (registry *Registry) Repositories() ([]string, error) { |
|
||||||
url := registry.url("/v2/_catalog") |
|
||||||
repos := make([]string, 0, 10) |
|
||||||
var err error //We create this here, otherwise url will be rescoped with :=
|
|
||||||
var response repositoriesResponse |
|
||||||
for { |
|
||||||
registry.Logf("registry.repositories url=%s", url) |
|
||||||
url, err = registry.getPaginatedJSON(url, &response) |
|
||||||
switch err { |
|
||||||
case ErrNoMorePages: |
|
||||||
repos = append(repos, response.Repositories...) |
|
||||||
return repos, nil |
|
||||||
case nil: |
|
||||||
repos = append(repos, response.Repositories...) |
|
||||||
continue |
|
||||||
default: |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,25 +0,0 @@ |
|||||||
package registry |
|
||||||
|
|
||||||
type tagsResponse struct { |
|
||||||
Tags []string `json:"tags"` |
|
||||||
} |
|
||||||
|
|
||||||
func (registry *Registry) Tags(repository string) (tags []string, err error) { |
|
||||||
url := registry.url("/v2/%s/tags/list", repository) |
|
||||||
|
|
||||||
var response tagsResponse |
|
||||||
for { |
|
||||||
registry.Logf("registry.tags url=%s repository=%s", url, repository) |
|
||||||
url, err = registry.getPaginatedJSON(url, &response) |
|
||||||
switch err { |
|
||||||
case ErrNoMorePages: |
|
||||||
tags = append(tags, response.Tags...) |
|
||||||
return tags, nil |
|
||||||
case nil: |
|
||||||
tags = append(tags, response.Tags...) |
|
||||||
continue |
|
||||||
default: |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,128 +0,0 @@ |
|||||||
package registry |
|
||||||
|
|
||||||
import ( |
|
||||||
"encoding/json" |
|
||||||
"fmt" |
|
||||||
"net/http" |
|
||||||
"net/url" |
|
||||||
) |
|
||||||
|
|
||||||
type TokenTransport struct { |
|
||||||
Transport http.RoundTripper |
|
||||||
Username string |
|
||||||
Password string |
|
||||||
} |
|
||||||
|
|
||||||
func (t *TokenTransport) RoundTrip(req *http.Request) (*http.Response, error) { |
|
||||||
resp, err := t.Transport.RoundTrip(req) |
|
||||||
if err != nil { |
|
||||||
return resp, err |
|
||||||
} |
|
||||||
if authService := isTokenDemand(resp); authService != nil { |
|
||||||
resp.Body.Close() |
|
||||||
resp, err = t.authAndRetry(authService, req) |
|
||||||
} |
|
||||||
return resp, err |
|
||||||
} |
|
||||||
|
|
||||||
type authToken struct { |
|
||||||
Token string `json:"token"` |
|
||||||
} |
|
||||||
|
|
||||||
func (t *TokenTransport) authAndRetry(authService *authService, req *http.Request) (*http.Response, error) { |
|
||||||
token, authResp, err := t.auth(authService) |
|
||||||
if err != nil { |
|
||||||
return authResp, err |
|
||||||
} |
|
||||||
|
|
||||||
retryResp, err := t.retry(req, token) |
|
||||||
return retryResp, err |
|
||||||
} |
|
||||||
|
|
||||||
func (t *TokenTransport) auth(authService *authService) (string, *http.Response, error) { |
|
||||||
authReq, err := authService.Request(t.Username, t.Password) |
|
||||||
if err != nil { |
|
||||||
return "", nil, err |
|
||||||
} |
|
||||||
|
|
||||||
client := http.Client{ |
|
||||||
Transport: t.Transport, |
|
||||||
} |
|
||||||
|
|
||||||
response, err := client.Do(authReq) |
|
||||||
if err != nil { |
|
||||||
return "", nil, err |
|
||||||
} |
|
||||||
|
|
||||||
if response.StatusCode != http.StatusOK { |
|
||||||
return "", response, err |
|
||||||
} |
|
||||||
defer response.Body.Close() |
|
||||||
|
|
||||||
var authToken authToken |
|
||||||
decoder := json.NewDecoder(response.Body) |
|
||||||
err = decoder.Decode(&authToken) |
|
||||||
if err != nil { |
|
||||||
return "", nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return authToken.Token, nil, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (t *TokenTransport) retry(req *http.Request, token string) (*http.Response, error) { |
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) |
|
||||||
resp, err := t.Transport.RoundTrip(req) |
|
||||||
return resp, err |
|
||||||
} |
|
||||||
|
|
||||||
type authService struct { |
|
||||||
Realm string |
|
||||||
Service string |
|
||||||
Scope string |
|
||||||
} |
|
||||||
|
|
||||||
func (authService *authService) Request(username, password string) (*http.Request, error) { |
|
||||||
url, err := url.Parse(authService.Realm) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
q := url.Query() |
|
||||||
q.Set("service", authService.Service) |
|
||||||
if authService.Scope != "" { |
|
||||||
q.Set("scope", authService.Scope) |
|
||||||
} |
|
||||||
url.RawQuery = q.Encode() |
|
||||||
|
|
||||||
request, err := http.NewRequest("GET", url.String(), nil) |
|
||||||
|
|
||||||
if username != "" || password != "" { |
|
||||||
request.SetBasicAuth(username, password) |
|
||||||
} |
|
||||||
|
|
||||||
return request, err |
|
||||||
} |
|
||||||
|
|
||||||
func isTokenDemand(resp *http.Response) *authService { |
|
||||||
if resp == nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
if resp.StatusCode != http.StatusUnauthorized { |
|
||||||
return nil |
|
||||||
} |
|
||||||
return parseOauthHeader(resp) |
|
||||||
} |
|
||||||
|
|
||||||
func parseOauthHeader(resp *http.Response) *authService { |
|
||||||
challenges := parseAuthHeader(resp.Header) |
|
||||||
for _, challenge := range challenges { |
|
||||||
if challenge.Scheme == "bearer" { |
|
||||||
return &authService{ |
|
||||||
Realm: challenge.Parameters["realm"], |
|
||||||
Service: challenge.Parameters["service"], |
|
||||||
Scope: challenge.Parameters["scope"], |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
@ -0,0 +1,40 @@ |
|||||||
|
/* |
||||||
|
Copyright © 2020-2021 The k3d Author(s) |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in |
||||||
|
all copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||||
|
THE SOFTWARE. |
||||||
|
*/ |
||||||
|
package version |
||||||
|
|
||||||
|
import ( |
||||||
|
"regexp" |
||||||
|
"testing" |
||||||
|
) |
||||||
|
|
||||||
|
// GetK3sVersion returns the version string for K3s
|
||||||
|
func TestGetK3sVersion(t *testing.T) { |
||||||
|
searchVersion := "v1.20" |
||||||
|
latestRegexp := regexp.MustCompile(`v1\.20\..*`) |
||||||
|
v, err := GetK3sVersion(searchVersion) |
||||||
|
if err != nil { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
if !latestRegexp.Match([]byte(v)) { |
||||||
|
t.Fatalf("found version %s (searched: %s) does not match respective latest regexp `%s`", v, searchVersion, latestRegexp.String()) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue