-
Notifications
You must be signed in to change notification settings - Fork 138
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add signing certificate to envelope (#330)
* add signing certificate to envelope Signed-off-by: Asra Ali <asraa@google.com>
- Loading branch information
Showing
5 changed files
with
291 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
package envelope | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
|
||
"github.com/secure-systems-lab/go-securesystemslib/dsse" | ||
"github.com/sigstore/sigstore/pkg/cryptoutils" | ||
) | ||
|
||
/* | ||
Envelope captures an envelope as described by the Secure Systems Lab | ||
Signing Specification. See here: | ||
https://github.com/secure-systems-lab/signing-spec/blob/master/envelope.md | ||
*/ | ||
type Envelope struct { | ||
PayloadType string `json:"payloadType"` | ||
Payload string `json:"payload"` | ||
Signatures []Signature `json:"signatures"` | ||
} | ||
|
||
/* | ||
Signature represents a generic in-toto signature that contains the identifier | ||
of the key which was used to create the signature. | ||
The used signature scheme has to be agreed upon by the signer and verifer | ||
out of band. | ||
The signature is a base64 encoding of the raw bytes from the signature | ||
algorithm. | ||
The cert is a PEM encoded string of the signing certificate | ||
*/ | ||
type Signature struct { | ||
KeyID string `json:"keyid"` | ||
Sig string `json:"sig"` | ||
Cert string `json:"cert"` | ||
} | ||
|
||
// AddCertToEnvelope takes a signed DSSE Envelope and a PEM-encoded certificate, and | ||
// returns an Envelope with the certificate inside the Signature of the Envelope. | ||
// This assumes there is only one signature present in the envelope. | ||
func AddCertToEnvelope(signedAtt []byte, cert []byte) ([]byte, error) { | ||
// Unmarshal into a DSSE envelope. | ||
env := &dsse.Envelope{} | ||
if err := json.Unmarshal(signedAtt, env); err != nil { | ||
return nil, err | ||
} | ||
|
||
// Create an envelope.Envelope. | ||
envWithCert := &Envelope{ | ||
PayloadType: env.PayloadType, | ||
Payload: env.Payload, | ||
Signatures: []Signature{}, | ||
} | ||
|
||
if len(env.Signatures) != 1 { | ||
return nil, fmt.Errorf("expected exactly one signature in the envelope") | ||
} | ||
|
||
if certs, err := cryptoutils.UnmarshalCertificatesFromPEM(cert); err != nil || len(certs) != 1 { | ||
return nil, fmt.Errorf("invalid certificate, expected PEM encoded certificate") | ||
} | ||
|
||
for _, sig := range env.Signatures { | ||
envWithCert.Signatures = append(envWithCert.Signatures, | ||
Signature{Sig: sig.Sig, KeyID: sig.KeyID, Cert: string(cert)}) | ||
} | ||
|
||
// Return marshalled result | ||
return json.Marshal(envWithCert) | ||
} | ||
|
||
// GetCertFromEnvelope takes a signed Envelope and extracts the PEM-encoded | ||
// certificate from the signature. | ||
// This assumes there is only one signature present in the envelope. | ||
func GetCertFromEnvelope(signedAtt []byte) ([]byte, error) { | ||
// Unmarshal into an envelope. | ||
env := &Envelope{} | ||
if err := json.Unmarshal(signedAtt, env); err != nil { | ||
return nil, err | ||
} | ||
|
||
if len(env.Signatures) != 1 { | ||
return nil, fmt.Errorf("expected exactly one signature in the envelope") | ||
} | ||
|
||
return []byte(env.Signatures[0].Cert), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
package envelope | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"crypto" | ||
"crypto/ecdsa" | ||
"crypto/elliptic" | ||
"crypto/rand" | ||
"crypto/x509" | ||
"encoding/json" | ||
"encoding/pem" | ||
"math/big" | ||
"testing" | ||
|
||
"github.com/go-openapi/strfmt" | ||
"github.com/go-openapi/swag" | ||
"github.com/in-toto/in-toto-golang/in_toto" | ||
"github.com/secure-systems-lab/go-securesystemslib/dsse" | ||
sdsse "github.com/sigstore/sigstore/pkg/signature/dsse" | ||
|
||
intoto "github.com/in-toto/in-toto-golang/in_toto" | ||
"github.com/sigstore/rekor/pkg/generated/models" | ||
"github.com/sigstore/rekor/pkg/types" | ||
intotod "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1" | ||
"github.com/sigstore/sigstore/pkg/signature" | ||
) | ||
|
||
func intotoEntry(certPem []byte, provenance []byte) (*intotod.V001Entry, error) { | ||
cert := strfmt.Base64(certPem) | ||
return &intotod.V001Entry{ | ||
IntotoObj: models.IntotoV001Schema{ | ||
Content: &models.IntotoV001SchemaContent{ | ||
Envelope: string(provenance), | ||
}, | ||
PublicKey: &cert, | ||
}, | ||
}, nil | ||
} | ||
|
||
// marshals a dsse envelope for testing | ||
func marshalEnvelope(t *testing.T, env *dsse.Envelope) string { | ||
b, err := json.Marshal(env) | ||
if err != nil { | ||
t.Fatalf("marshalling envelope: %s", err) | ||
} | ||
return string(b) | ||
} | ||
|
||
// test utility to sign a payload with a given signer | ||
func envelope(t *testing.T, k *ecdsa.PrivateKey, payload []byte) string { | ||
s, err := signature.LoadECDSASigner(k, crypto.SHA256) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
wrappedSigner := sdsse.WrapSigner(s, intoto.PayloadType) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
dsseEnv, err := wrappedSigner.SignMessage(bytes.NewReader(payload)) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
return string(dsseEnv) | ||
} | ||
|
||
func TestAddCert(t *testing.T) { | ||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
ca := &x509.Certificate{ | ||
SerialNumber: big.NewInt(1), | ||
} | ||
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &priv.PublicKey, priv) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
certPemBytes := pem.EncodeToMemory(&pem.Block{ | ||
Type: "CERTIFICATE", | ||
Bytes: caBytes, | ||
}) | ||
validPayload := "hellothispayloadisvalid" | ||
|
||
tests := []struct { | ||
name string | ||
env string | ||
cert []byte | ||
addErr bool | ||
}{ | ||
{ | ||
name: "invalid empty envelope with no signatures", | ||
env: marshalEnvelope(t, &dsse.Envelope{}), | ||
cert: nil, | ||
addErr: true, | ||
}, | ||
{ | ||
name: "invalid envelope with two signatures", | ||
env: marshalEnvelope(t, &dsse.Envelope{ | ||
Payload: "", | ||
PayloadType: in_toto.PayloadType, | ||
Signatures: []dsse.Signature{ | ||
{ | ||
Sig: "abc", | ||
}, | ||
{ | ||
Sig: "xyz", | ||
}, | ||
}, | ||
}), | ||
cert: nil, | ||
addErr: true, | ||
}, | ||
{ | ||
name: "invalid cert with valid envelope", | ||
env: marshalEnvelope(t, &dsse.Envelope{ | ||
Payload: "", | ||
PayloadType: in_toto.PayloadType, | ||
Signatures: []dsse.Signature{ | ||
{ | ||
Sig: "abc", | ||
}, | ||
}, | ||
}), | ||
cert: nil, | ||
addErr: true, | ||
}, | ||
{ | ||
name: "valid envelope", | ||
env: envelope(t, priv, []byte(validPayload)), | ||
cert: certPemBytes, | ||
addErr: false, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
// Add certificate to envelope. | ||
envWithCert, err := AddCertToEnvelope([]byte(tt.env), tt.cert) | ||
if (err != nil) != tt.addErr { | ||
t.Errorf("AddCertToEnvelope() error = %v, wanted %v", err, tt.addErr) | ||
} | ||
if err != nil { | ||
return | ||
} | ||
|
||
// Now get cert from envelope and compare. | ||
gotCert, err := GetCertFromEnvelope(envWithCert) | ||
if err != nil { | ||
t.Fatalf("GetCertFromEnvelope() error = %v", err) | ||
} | ||
|
||
if !bytes.EqualFold(gotCert, tt.cert) { | ||
t.Errorf("expected cert equality") | ||
} | ||
|
||
// Now test compatibility with Rekor intoto entry type. | ||
testRekorSupport(t, tt.cert, envWithCert) | ||
}) | ||
} | ||
} | ||
|
||
// This servers as a regression test to make sure that the Rekor intoto | ||
// type can successfully unmarshal our "Envelope" with included cert. | ||
func testRekorSupport(t *testing.T, certPem []byte, envWithCert []byte) { | ||
ctx := context.Background() | ||
intotoEntry, err := intotoEntry(certPem, envWithCert) | ||
if err != nil { | ||
t.Fatalf("error creating intoto entry: %s", err) | ||
} | ||
e := models.Intoto{ | ||
APIVersion: swag.String(intotoEntry.APIVersion()), | ||
Spec: intotoEntry.IntotoObj, | ||
} | ||
pe := models.ProposedEntry(&e) | ||
entry, err := types.NewEntry(pe) | ||
if err != nil { | ||
t.Fatalf("error creating valid intoto entry") | ||
} | ||
_, err = types.CanonicalizeEntry(ctx, entry) | ||
if err != nil { | ||
t.Fatalf("error creating valid intoto entry") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters