2016-11-22 19:32:10 +00:00
|
|
|
|
// Note: Consider the API unstable until the code supports at least three different image formats or transports.
|
|
|
|
|
|
2017-05-17 17:18:35 +00:00
|
|
|
|
// NOTE: Keep this in sync with docs/atomic-signature.md and docs/atomic-signature-embedded.json!
|
|
|
|
|
|
2016-11-22 19:32:10 +00:00
|
|
|
|
package signature
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"time"
|
|
|
|
|
|
2016-10-17 13:53:40 +00:00
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
|
|
2016-11-22 19:32:10 +00:00
|
|
|
|
"github.com/containers/image/version"
|
2016-10-17 13:53:40 +00:00
|
|
|
|
"github.com/opencontainers/go-digest"
|
2016-11-22 19:32:10 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
signatureType = "atomic container signature"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// InvalidSignatureError is returned when parsing an invalid signature.
|
|
|
|
|
type InvalidSignatureError struct {
|
|
|
|
|
msg string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (err InvalidSignatureError) Error() string {
|
|
|
|
|
return err.msg
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Signature is a parsed content of a signature.
|
2017-02-01 00:45:59 +00:00
|
|
|
|
// The only way to get this structure from a blob should be as a return value from a successful call to verifyAndExtractSignature below.
|
2016-11-22 19:32:10 +00:00
|
|
|
|
type Signature struct {
|
|
|
|
|
DockerManifestDigest digest.Digest
|
|
|
|
|
DockerReference string // FIXME: more precise type?
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-01 00:45:59 +00:00
|
|
|
|
// untrustedSignature is a parsed content of a signature.
|
|
|
|
|
type untrustedSignature struct {
|
|
|
|
|
UntrustedDockerManifestDigest digest.Digest
|
|
|
|
|
UntrustedDockerReference string // FIXME: more precise type?
|
|
|
|
|
UntrustedCreatorID *string
|
|
|
|
|
// This is intentionally an int64; the native JSON float64 type would allow to represent _some_ sub-second precision,
|
|
|
|
|
// but not nearly enough (with current timestamp values, a single unit in the last place is on the order of hundreds of nanoseconds).
|
|
|
|
|
// So, this is explicitly an int64, and we reject fractional values. If we did need more precise timestamps eventually,
|
|
|
|
|
// we would add another field, UntrustedTimestampNS int64.
|
|
|
|
|
UntrustedTimestamp *int64
|
2016-11-22 19:32:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-02-01 00:45:59 +00:00
|
|
|
|
// UntrustedSignatureInformation is information available in an untrusted signature.
|
|
|
|
|
// This may be useful when debugging signature verification failures,
|
|
|
|
|
// or when managing a set of signatures on a single image.
|
|
|
|
|
//
|
|
|
|
|
// WARNING: Do not use the contents of this for ANY security decisions,
|
|
|
|
|
// and be VERY CAREFUL about showing this information to humans in any way which suggest that these values “are probably” reliable.
|
|
|
|
|
// There is NO REASON to expect the values to be correct, or not intentionally misleading
|
|
|
|
|
// (including things like “✅ Verified by $authority”)
|
|
|
|
|
type UntrustedSignatureInformation struct {
|
|
|
|
|
UntrustedDockerManifestDigest digest.Digest
|
|
|
|
|
UntrustedDockerReference string // FIXME: more precise type?
|
|
|
|
|
UntrustedCreatorID *string
|
|
|
|
|
UntrustedTimestamp *time.Time
|
|
|
|
|
UntrustedShortKeyIdentifier string
|
|
|
|
|
}
|
2016-11-22 19:32:10 +00:00
|
|
|
|
|
2017-02-01 00:45:59 +00:00
|
|
|
|
// newUntrustedSignature returns an untrustedSignature object with
|
|
|
|
|
// the specified primary contents and appropriate metadata.
|
|
|
|
|
func newUntrustedSignature(dockerManifestDigest digest.Digest, dockerReference string) untrustedSignature {
|
|
|
|
|
// Use intermediate variables for these values so that we can take their addresses.
|
|
|
|
|
// Golang guarantees that they will have a new address on every execution.
|
|
|
|
|
creatorID := "atomic " + version.Version
|
|
|
|
|
timestamp := time.Now().Unix()
|
|
|
|
|
return untrustedSignature{
|
|
|
|
|
UntrustedDockerManifestDigest: dockerManifestDigest,
|
|
|
|
|
UntrustedDockerReference: dockerReference,
|
|
|
|
|
UntrustedCreatorID: &creatorID,
|
|
|
|
|
UntrustedTimestamp: ×tamp,
|
|
|
|
|
}
|
2016-11-22 19:32:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-02-01 00:45:59 +00:00
|
|
|
|
// Compile-time check that untrustedSignature implements json.Marshaler
|
|
|
|
|
var _ json.Marshaler = (*untrustedSignature)(nil)
|
|
|
|
|
|
|
|
|
|
// MarshalJSON implements the json.Marshaler interface.
|
|
|
|
|
func (s untrustedSignature) MarshalJSON() ([]byte, error) {
|
|
|
|
|
if s.UntrustedDockerManifestDigest == "" || s.UntrustedDockerReference == "" {
|
2016-11-22 19:32:10 +00:00
|
|
|
|
return nil, errors.New("Unexpected empty signature content")
|
|
|
|
|
}
|
|
|
|
|
critical := map[string]interface{}{
|
|
|
|
|
"type": signatureType,
|
2017-02-01 00:45:59 +00:00
|
|
|
|
"image": map[string]string{"docker-manifest-digest": s.UntrustedDockerManifestDigest.String()},
|
|
|
|
|
"identity": map[string]string{"docker-reference": s.UntrustedDockerReference},
|
|
|
|
|
}
|
|
|
|
|
optional := map[string]interface{}{}
|
|
|
|
|
if s.UntrustedCreatorID != nil {
|
|
|
|
|
optional["creator"] = *s.UntrustedCreatorID
|
2016-11-22 19:32:10 +00:00
|
|
|
|
}
|
2017-02-01 00:45:59 +00:00
|
|
|
|
if s.UntrustedTimestamp != nil {
|
|
|
|
|
optional["timestamp"] = *s.UntrustedTimestamp
|
2016-11-22 19:32:10 +00:00
|
|
|
|
}
|
|
|
|
|
signature := map[string]interface{}{
|
|
|
|
|
"critical": critical,
|
|
|
|
|
"optional": optional,
|
|
|
|
|
}
|
|
|
|
|
return json.Marshal(signature)
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-01 00:45:59 +00:00
|
|
|
|
// Compile-time check that untrustedSignature implements json.Unmarshaler
|
|
|
|
|
var _ json.Unmarshaler = (*untrustedSignature)(nil)
|
2016-11-22 19:32:10 +00:00
|
|
|
|
|
|
|
|
|
// UnmarshalJSON implements the json.Unmarshaler interface
|
2017-02-01 00:45:59 +00:00
|
|
|
|
func (s *untrustedSignature) UnmarshalJSON(data []byte) error {
|
2016-11-22 19:32:10 +00:00
|
|
|
|
err := s.strictUnmarshalJSON(data)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if _, ok := err.(jsonFormatError); ok {
|
|
|
|
|
err = InvalidSignatureError{msg: err.Error()}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// strictUnmarshalJSON is UnmarshalJSON, except that it may return the internal jsonFormatError error type.
|
|
|
|
|
// Splitting it into a separate function allows us to do the jsonFormatError → InvalidSignatureError in a single place, the caller.
|
2017-02-01 00:45:59 +00:00
|
|
|
|
func (s *untrustedSignature) strictUnmarshalJSON(data []byte) error {
|
2017-04-03 07:22:44 +00:00
|
|
|
|
var critical, optional json.RawMessage
|
|
|
|
|
if err := paranoidUnmarshalJSONObjectExactFields(data, map[string]interface{}{
|
|
|
|
|
"critical": &critical,
|
|
|
|
|
"optional": &optional,
|
|
|
|
|
}); err != nil {
|
2016-11-22 19:32:10 +00:00
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-03 07:22:44 +00:00
|
|
|
|
var creatorID string
|
|
|
|
|
var timestamp float64
|
|
|
|
|
var gotCreatorID, gotTimestamp = false, false
|
|
|
|
|
if err := paranoidUnmarshalJSONObject(optional, func(key string) interface{} {
|
|
|
|
|
switch key {
|
|
|
|
|
case "creator":
|
|
|
|
|
gotCreatorID = true
|
|
|
|
|
return &creatorID
|
|
|
|
|
case "timestamp":
|
|
|
|
|
gotTimestamp = true
|
|
|
|
|
return ×tamp
|
|
|
|
|
default:
|
|
|
|
|
var ignore interface{}
|
|
|
|
|
return &ignore
|
|
|
|
|
}
|
|
|
|
|
}); err != nil {
|
2016-11-22 19:32:10 +00:00
|
|
|
|
return err
|
|
|
|
|
}
|
2017-04-03 07:22:44 +00:00
|
|
|
|
if gotCreatorID {
|
2017-02-01 00:45:59 +00:00
|
|
|
|
s.UntrustedCreatorID = &creatorID
|
|
|
|
|
}
|
2017-04-03 07:22:44 +00:00
|
|
|
|
if gotTimestamp {
|
|
|
|
|
intTimestamp := int64(timestamp)
|
|
|
|
|
if float64(intTimestamp) != timestamp {
|
|
|
|
|
return InvalidSignatureError{msg: "Field optional.timestamp is not is not an integer"}
|
2017-02-01 00:45:59 +00:00
|
|
|
|
}
|
2017-04-03 07:22:44 +00:00
|
|
|
|
s.UntrustedTimestamp = &intTimestamp
|
2017-02-01 00:45:59 +00:00
|
|
|
|
}
|
2016-11-22 19:32:10 +00:00
|
|
|
|
|
2017-04-03 07:22:44 +00:00
|
|
|
|
var t string
|
|
|
|
|
var image, identity json.RawMessage
|
|
|
|
|
if err := paranoidUnmarshalJSONObjectExactFields(critical, map[string]interface{}{
|
|
|
|
|
"type": &t,
|
|
|
|
|
"image": &image,
|
|
|
|
|
"identity": &identity,
|
|
|
|
|
}); err != nil {
|
2016-11-22 19:32:10 +00:00
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if t != signatureType {
|
|
|
|
|
return InvalidSignatureError{msg: fmt.Sprintf("Unrecognized signature type %s", t)}
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-03 07:22:44 +00:00
|
|
|
|
var digestString string
|
|
|
|
|
if err := paranoidUnmarshalJSONObjectExactFields(image, map[string]interface{}{
|
|
|
|
|
"docker-manifest-digest": &digestString,
|
|
|
|
|
}); err != nil {
|
2016-11-22 19:32:10 +00:00
|
|
|
|
return err
|
|
|
|
|
}
|
2017-02-01 00:45:59 +00:00
|
|
|
|
s.UntrustedDockerManifestDigest = digest.Digest(digestString)
|
2016-11-22 19:32:10 +00:00
|
|
|
|
|
2017-10-10 14:11:06 +00:00
|
|
|
|
return paranoidUnmarshalJSONObjectExactFields(identity, map[string]interface{}{
|
2017-04-03 07:22:44 +00:00
|
|
|
|
"docker-reference": &s.UntrustedDockerReference,
|
2017-10-10 14:11:06 +00:00
|
|
|
|
})
|
2016-11-22 19:32:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sign formats the signature and returns a blob signed using mech and keyIdentity
|
2017-02-01 00:45:59 +00:00
|
|
|
|
// (If it seems surprising that this is a method on untrustedSignature, note that there
|
|
|
|
|
// isn’t a good reason to think that a key used by the user is trusted by any component
|
|
|
|
|
// of the system just because it is a private key — actually the presence of a private key
|
|
|
|
|
// on the system increases the likelihood of an a successful attack on that private key
|
|
|
|
|
// on that particular system.)
|
|
|
|
|
func (s untrustedSignature) sign(mech SigningMechanism, keyIdentity string) ([]byte, error) {
|
2016-11-22 19:32:10 +00:00
|
|
|
|
json, err := json.Marshal(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return mech.Sign(json, keyIdentity)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// signatureAcceptanceRules specifies how to decide whether an untrusted signature is acceptable.
|
|
|
|
|
// We centralize the actual parsing and data extraction in verifyAndExtractSignature; this supplies
|
|
|
|
|
// the policy. We use an object instead of supplying func parameters to verifyAndExtractSignature
|
2017-02-01 00:45:59 +00:00
|
|
|
|
// because the functions have the same or similar types, so there is a risk of exchanging the functions;
|
2016-11-22 19:32:10 +00:00
|
|
|
|
// named members of this struct are more explicit.
|
|
|
|
|
type signatureAcceptanceRules struct {
|
|
|
|
|
validateKeyIdentity func(string) error
|
|
|
|
|
validateSignedDockerReference func(string) error
|
|
|
|
|
validateSignedDockerManifestDigest func(digest.Digest) error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// verifyAndExtractSignature verifies that unverifiedSignature has been signed, and that its principial components
|
|
|
|
|
// match expected values, both as specified by rules, and returns it
|
|
|
|
|
func verifyAndExtractSignature(mech SigningMechanism, unverifiedSignature []byte, rules signatureAcceptanceRules) (*Signature, error) {
|
|
|
|
|
signed, keyIdentity, err := mech.Verify(unverifiedSignature)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if err := rules.validateKeyIdentity(keyIdentity); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-01 00:45:59 +00:00
|
|
|
|
var unmatchedSignature untrustedSignature
|
2016-11-22 19:32:10 +00:00
|
|
|
|
if err := json.Unmarshal(signed, &unmatchedSignature); err != nil {
|
|
|
|
|
return nil, InvalidSignatureError{msg: err.Error()}
|
|
|
|
|
}
|
2017-02-01 00:45:59 +00:00
|
|
|
|
if err := rules.validateSignedDockerManifestDigest(unmatchedSignature.UntrustedDockerManifestDigest); err != nil {
|
2016-11-22 19:32:10 +00:00
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2017-02-01 00:45:59 +00:00
|
|
|
|
if err := rules.validateSignedDockerReference(unmatchedSignature.UntrustedDockerReference); err != nil {
|
2016-11-22 19:32:10 +00:00
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2017-02-01 00:45:59 +00:00
|
|
|
|
// signatureAcceptanceRules have accepted this value.
|
|
|
|
|
return &Signature{
|
|
|
|
|
DockerManifestDigest: unmatchedSignature.UntrustedDockerManifestDigest,
|
|
|
|
|
DockerReference: unmatchedSignature.UntrustedDockerReference,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetUntrustedSignatureInformationWithoutVerifying extracts information available in an untrusted signature,
|
|
|
|
|
// WITHOUT doing any cryptographic verification.
|
|
|
|
|
// This may be useful when debugging signature verification failures,
|
|
|
|
|
// or when managing a set of signatures on a single image.
|
|
|
|
|
//
|
|
|
|
|
// WARNING: Do not use the contents of this for ANY security decisions,
|
|
|
|
|
// and be VERY CAREFUL about showing this information to humans in any way which suggest that these values “are probably” reliable.
|
|
|
|
|
// There is NO REASON to expect the values to be correct, or not intentionally misleading
|
|
|
|
|
// (including things like “✅ Verified by $authority”)
|
|
|
|
|
func GetUntrustedSignatureInformationWithoutVerifying(untrustedSignatureBytes []byte) (*UntrustedSignatureInformation, error) {
|
|
|
|
|
// NOTE: This should eventualy do format autodetection.
|
2017-04-03 07:22:44 +00:00
|
|
|
|
mech, _, err := NewEphemeralGPGSigningMechanism([]byte{})
|
2017-02-01 00:45:59 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2017-04-03 07:22:44 +00:00
|
|
|
|
defer mech.Close()
|
2017-02-01 00:45:59 +00:00
|
|
|
|
|
|
|
|
|
untrustedContents, shortKeyIdentifier, err := mech.UntrustedSignatureContents(untrustedSignatureBytes)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
var untrustedDecodedContents untrustedSignature
|
|
|
|
|
if err := json.Unmarshal(untrustedContents, &untrustedDecodedContents); err != nil {
|
|
|
|
|
return nil, InvalidSignatureError{msg: err.Error()}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var timestamp *time.Time // = nil
|
|
|
|
|
if untrustedDecodedContents.UntrustedTimestamp != nil {
|
|
|
|
|
ts := time.Unix(*untrustedDecodedContents.UntrustedTimestamp, 0)
|
|
|
|
|
timestamp = &ts
|
|
|
|
|
}
|
|
|
|
|
return &UntrustedSignatureInformation{
|
|
|
|
|
UntrustedDockerManifestDigest: untrustedDecodedContents.UntrustedDockerManifestDigest,
|
|
|
|
|
UntrustedDockerReference: untrustedDecodedContents.UntrustedDockerReference,
|
|
|
|
|
UntrustedCreatorID: untrustedDecodedContents.UntrustedCreatorID,
|
|
|
|
|
UntrustedTimestamp: timestamp,
|
|
|
|
|
UntrustedShortKeyIdentifier: shortKeyIdentifier,
|
|
|
|
|
}, nil
|
2016-11-22 19:32:10 +00:00
|
|
|
|
}
|