1414package main
1515
1616import (
17+ "crypto/x509"
1718 "errors"
1819 "fmt"
20+ "net/http"
1921 "os"
2022 "strings"
2123 "time"
2224
25+ corex509 "github.com/notaryproject/notation-core-go/x509"
2326 "github.com/notaryproject/notation-go"
2427 "github.com/notaryproject/notation/cmd/notation/internal/experimental"
2528 "github.com/notaryproject/notation/internal/cmd"
2629 "github.com/notaryproject/notation/internal/envelope"
30+ "github.com/notaryproject/notation/internal/httputil"
31+ nx509 "github.com/notaryproject/notation/internal/x509"
32+ "github.com/notaryproject/tspclient-go"
2733 ocispec "github.com/opencontainers/image-spec/specs-go/v1"
2834 "github.com/spf13/cobra"
35+ "golang.org/x/net/context"
2936)
3037
3138const referrersTagSchemaDeleteError = "failed to delete dangling referrers index"
3239
40+ // timestampingTimeout is the timeout when requesting timestamp countersignature
41+ // from a TSA
42+ const timestampingTimeout = 15 * time .Second
43+
3344type signOpts struct {
3445 cmd.LoggingFlagOpts
3546 cmd.SignerFlagOpts
3647 SecureFlagOpts
37- expiry time.Duration
38- pluginConfig []string
39- userMetadata []string
40- reference string
41- allowReferrersAPI bool
42- forceReferrersTag bool
43- ociLayout bool
44- inputType inputType
48+ expiry time.Duration
49+ pluginConfig []string
50+ userMetadata []string
51+ reference string
52+ allowReferrersAPI bool
53+ forceReferrersTag bool
54+ ociLayout bool
55+ inputType inputType
56+ tsaServerURL string
57+ tsaRootCertificatePath string
4558}
4659
4760func signCommand (opts * signOpts ) * cobra.Command {
@@ -74,6 +87,9 @@ Example - Sign an OCI artifact stored in a registry and specify the signature ex
7487
7588Example - Sign an OCI artifact and store signature using the Referrers API. If it's not supported, fallback to the Referrers tag schema
7689 notation sign --force-referrers-tag=false <registry>/<repository>@<digest>
90+
91+ Example - Sign an OCI artifact with timestamping:
92+ notation sign --timestamp-url <TSA_url> --timestamp-root-cert <TSA_root_certificate_filepath> <registry>/<repository>@<digest>
7793`
7894 experimentalExamples := `
7995Example - [Experimental] Sign an OCI artifact referenced in an OCI layout
@@ -101,6 +117,16 @@ Example - [Experimental] Sign an OCI artifact identified by a tag and referenced
101117 return experimental .CheckFlagsAndWarn (cmd , "allow-referrers-api" , "oci-layout" )
102118 },
103119 RunE : func (cmd * cobra.Command , args []string ) error {
120+ // timestamping
121+ if cmd .Flags ().Changed ("timestamp-url" ) {
122+ if opts .tsaServerURL == "" {
123+ return errors .New ("timestamping: tsa url cannot be empty" )
124+ }
125+ if opts .tsaRootCertificatePath == "" {
126+ return errors .New ("timestamping: tsa root certificate path cannot be empty" )
127+ }
128+ }
129+
104130 // allow-referrers-api flag is set
105131 if cmd .Flags ().Changed ("allow-referrers-api" ) {
106132 if opts .allowReferrersAPI {
@@ -120,9 +146,12 @@ Example - [Experimental] Sign an OCI artifact identified by a tag and referenced
120146 cmd .SetPflagPluginConfig (command .Flags (), & opts .pluginConfig )
121147 cmd .SetPflagUserMetadata (command .Flags (), & opts .userMetadata , cmd .PflagUserMetadataSignUsage )
122148 cmd .SetPflagReferrersAPI (command .Flags (), & opts .allowReferrersAPI , fmt .Sprintf (cmd .PflagReferrersUsageFormat , "sign" ))
149+ command .Flags ().StringVar (& opts .tsaServerURL , "timestamp-url" , "" , "RFC 3161 Timestamping Authority (TSA) server URL" )
150+ command .Flags ().StringVar (& opts .tsaRootCertificatePath , "timestamp-root-cert" , "" , "filepath of timestamp authority root certificate" )
123151 cmd .SetPflagReferrersTag (command .Flags (), & opts .forceReferrersTag , "force to store signatures using the referrers tag schema" )
124152 command .Flags ().BoolVar (& opts .ociLayout , "oci-layout" , false , "[Experimental] sign the artifact stored as OCI image layout" )
125153 command .MarkFlagsMutuallyExclusive ("oci-layout" , "force-referrers-tag" , "allow-referrers-api" )
154+ command .MarkFlagsRequiredTogether ("timestamp-url" , "timestamp-root-cert" )
126155 experimental .HideFlags (command , experimentalExamples , []string {"oci-layout" })
127156 return command
128157}
@@ -140,7 +169,7 @@ func runSign(command *cobra.Command, cmdOpts *signOpts) error {
140169 if err != nil {
141170 return err
142171 }
143- signOpts , err := prepareSigningOpts (cmdOpts )
172+ signOpts , err := prepareSigningOpts (ctx , cmdOpts )
144173 if err != nil {
145174 return err
146175 }
@@ -168,7 +197,7 @@ func runSign(command *cobra.Command, cmdOpts *signOpts) error {
168197 return nil
169198}
170199
171- func prepareSigningOpts (opts * signOpts ) (notation.SignOptions , error ) {
200+ func prepareSigningOpts (ctx context. Context , opts * signOpts ) (notation.SignOptions , error ) {
172201 mediaType , err := envelope .GetEnvelopeMediaType (opts .SignerFlagOpts .SignatureFormat )
173202 if err != nil {
174203 return notation.SignOptions {}, err
@@ -189,5 +218,36 @@ func prepareSigningOpts(opts *signOpts) (notation.SignOptions, error) {
189218 },
190219 UserMetadata : userMetadata ,
191220 }
221+ if opts .tsaServerURL != "" {
222+ // timestamping
223+ fmt .Printf ("Configured to timestamp with TSA %q\n " , opts .tsaServerURL )
224+ signOpts .Timestamper , err = tspclient .NewHTTPTimestamper (httputil .NewClient (ctx , & http.Client {Timeout : timestampingTimeout }), opts .tsaServerURL )
225+ if err != nil {
226+ return notation.SignOptions {}, fmt .Errorf ("cannot get http timestamper for timestamping: %w" , err )
227+ }
228+
229+ rootCerts , err := corex509 .ReadCertificateFile (opts .tsaRootCertificatePath )
230+ if err != nil {
231+ return notation.SignOptions {}, err
232+ }
233+ if len (rootCerts ) == 0 {
234+ return notation.SignOptions {}, fmt .Errorf ("cannot find any certificate from %q. Expecting single x509 root certificate in PEM or DER format from the file" , opts .tsaRootCertificatePath )
235+ }
236+ if len (rootCerts ) > 1 {
237+ return notation.SignOptions {}, fmt .Errorf ("found more than one certificates from %q. Expecting single x509 root certificate in PEM or DER format from the file" , opts .tsaRootCertificatePath )
238+ }
239+ tsaRootCert := rootCerts [0 ]
240+ isRoot , err := nx509 .IsRootCertificate (tsaRootCert )
241+ if err != nil {
242+ return notation.SignOptions {}, fmt .Errorf ("failed to check root certificate with error: %w" , err )
243+ }
244+ if ! isRoot {
245+ return notation.SignOptions {}, fmt .Errorf ("certificate from %q is not a root certificate. Expecting single x509 root certificate in PEM or DER format from the file" , opts .tsaRootCertificatePath )
246+
247+ }
248+ rootCAs := x509 .NewCertPool ()
249+ rootCAs .AddCert (tsaRootCert )
250+ signOpts .TSARootCAs = rootCAs
251+ }
192252 return signOpts , nil
193253}
0 commit comments