11/*
2- Copyright 2023 The Sigstore Authors.
2+ Copyright 2025 The Sigstore Authors.
33
44Licensed under the Apache License, Version 2.0 (the "License");
55you may not use this file except in compliance with the License.
@@ -15,7 +15,14 @@ limitations under the License.
1515*/
1616import nock from 'nock' ;
1717import { InternalError } from '../../../error' ;
18- import { ProposedEntry , TLogClient } from '../../../witness/tlog/client' ;
18+ import {
19+ ProposedEntry ,
20+ TLogClient ,
21+ TLogV2Client ,
22+ } from '../../../witness/tlog/client' ;
23+
24+ import type { CreateEntryRequest } from '@sigstore/protobuf-specs/rekor/v2' ;
25+ import assert from 'assert' ;
1926
2027describe ( 'TLogClient' , ( ) => {
2128 const rekorBaseURL = 'http://localhost:8080' ;
@@ -194,3 +201,201 @@ describe('TLogClient', () => {
194201 } ) ;
195202 } ) ;
196203} ) ;
204+
205+ describe ( 'TLogV2Client' , ( ) => {
206+ const rekorBaseURL = 'http://localhost:8080' ;
207+
208+ describe ( 'constructor' , ( ) => {
209+ it ( 'should create a new instance' , ( ) => {
210+ const client = new TLogV2Client ( { rekorBaseURL } ) ;
211+ expect ( client ) . toBeDefined ( ) ;
212+ } ) ;
213+ } ) ;
214+
215+ describe ( 'createEntry' , ( ) => {
216+ const subject = new TLogV2Client ( { rekorBaseURL, retry : false } ) ;
217+
218+ const createEntryRequest : CreateEntryRequest = {
219+ spec : {
220+ $case : 'hashedRekordRequestV002' ,
221+ hashedRekordRequestV002 : {
222+ digest : Buffer . from ( 'digest' ) ,
223+ signature : {
224+ content : Buffer . from ( 'signature' ) ,
225+ verifier : {
226+ keyDetails : 5 , // PKIX_ECDSA_P256_SHA_256
227+ verifier : {
228+ $case : 'x509Certificate' ,
229+ x509Certificate : {
230+ rawBytes : Buffer . from ( 'certificate' ) ,
231+ } ,
232+ } ,
233+ } ,
234+ } ,
235+ } ,
236+ } ,
237+ } ;
238+
239+ const rekorEntry = {
240+ logIndex : '2513258' ,
241+ logId : {
242+ keyId : 'zxGZFVvd0FEmjR8WrFwMdcAJ9vtaY/QXf44Y1wUeP6A=' ,
243+ } ,
244+ kindVersion : {
245+ kind : 'hashedrekord' ,
246+ version : '0.0.2' ,
247+ } ,
248+ integratedTime : '0' ,
249+ inclusionPromise : null ,
250+ inclusionProof : {
251+ logIndex : '2513258' ,
252+ rootHash : '+8vUkEgBK/ansexBUomzocaWoPEmPIzxJC/y+xNMQN4=' ,
253+ treeSize : '412115' ,
254+ hashes : [
255+ 'KIoYVJ0TqmaEkFboP7YWTjSh8vFjVECmokcTOAByfIM=' ,
256+ 'Umf0h0cK2hegTzNnSgsXszyiA4bp5OvEvP+GrWq3C8w=' ,
257+ 'vNQeSNBfepYZI2Ez3ViKdCft0JH87ZS8IGKwixxUVjc=' ,
258+ 'JBAugd5awOqmHXIKgz1MOjlR5f37VqmP0bWoRVcHX5M=' ,
259+ 'xYH2mAxGxfOgvSOLnItT2LsJt+Z2a2egjf8QJFwK7jA=' ,
260+ 'PbZWM3NitzChx9A22m/kddDtzh2bAKX5Fy7j76l3z2k=' ,
261+ 'sKX9Sbsahvw5DiC2oP6pbZsDi1NzuNS1nIULXkCC57o=' ,
262+ 'pfTCxXHnCM253jwYxVJcuUhmoTTtDxznn92QhN0M4Ws=' ,
263+ 'cJ8uk2ZvZyfg+HRILKOHcyHu2pvI8Fz3R1MyMvyzWtA=' ,
264+ ] ,
265+ checkpoint : {
266+ envelope :
267+ 'log2025-1.rekor.sigstore.dev\n412115\n+8vUkEgBK/ansexBUomzocaWoPEmPIzxJC/y+xNMQN4=\n\n— log2025-1.rekor.sigstore.dev zxGZFahCZ/+MqTjH4rC5MWcdLDWbpetE5l30RZfQc4BQkRjWSoKipEUPjvHENeZDHIlAsuezJcLzUVvItpNjaSRoMAs=\n' ,
268+ } ,
269+ } ,
270+ canonicalizedBody : Buffer . from (
271+ JSON . stringify ( createEntryRequest )
272+ ) . toString ( 'base64' ) ,
273+ } ;
274+
275+ describe ( 'when Rekor returns an error' , ( ) => {
276+ beforeEach ( ( ) => {
277+ nock ( rekorBaseURL )
278+ . matchHeader ( 'Accept' , 'application/json' )
279+ . matchHeader ( 'Content-Type' , 'application/json' )
280+ . post ( '/api/v2/log/entries' )
281+ . reply ( 500 , { } ) ;
282+ } ) ;
283+
284+ it ( 'returns an error' , async ( ) => {
285+ await expect (
286+ subject . createEntry ( createEntryRequest )
287+ ) . rejects . toThrowWithCode ( InternalError , 'TLOG_CREATE_ENTRY_ERROR' ) ;
288+ } ) ;
289+ } ) ;
290+
291+ describe ( 'when Rekor returns a valid response' , ( ) => {
292+ beforeEach ( ( ) => {
293+ nock ( rekorBaseURL )
294+ . matchHeader ( 'Accept' , 'application/json' )
295+ . matchHeader ( 'Content-Type' , 'application/json' )
296+ . post ( '/api/v2/log/entries' )
297+ . reply ( 201 , rekorEntry ) ;
298+ } ) ;
299+
300+ it ( 'returns a tlog entry' , async ( ) => {
301+ const entry = await subject . createEntry ( createEntryRequest ) ;
302+
303+ expect ( entry . logIndex ) . toEqual ( rekorEntry . logIndex ) ;
304+ expect ( entry . logId . keyId ) . toEqual (
305+ Buffer . from ( rekorEntry . logId . keyId , 'base64' )
306+ ) ;
307+ expect ( entry . integratedTime ) . toEqual ( rekorEntry . integratedTime ) ;
308+ expect ( entry . kindVersion ) . toEqual ( rekorEntry . kindVersion ) ;
309+ expect ( entry . inclusionPromise ) . toBeUndefined ( ) ;
310+ assert ( entry . inclusionProof ) ;
311+ expect ( entry . inclusionProof . logIndex ) . toEqual (
312+ rekorEntry . inclusionProof . logIndex
313+ ) ;
314+ expect ( entry . inclusionProof . rootHash ) . toEqual (
315+ Buffer . from ( rekorEntry . inclusionProof . rootHash , 'base64' )
316+ ) ;
317+ expect ( entry . inclusionProof . treeSize ) . toEqual (
318+ rekorEntry . inclusionProof . treeSize
319+ ) ;
320+ expect ( entry . inclusionProof . hashes ) . toEqual (
321+ rekorEntry . inclusionProof . hashes . map ( ( h : string ) =>
322+ Buffer . from ( h , 'base64' )
323+ )
324+ ) ;
325+ assert ( entry . inclusionProof . checkpoint ) ;
326+ expect ( entry . inclusionProof . checkpoint . envelope ) . toEqual (
327+ rekorEntry . inclusionProof . checkpoint . envelope
328+ ) ;
329+ expect ( entry . canonicalizedBody ) . toEqual (
330+ Buffer . from ( rekorEntry . canonicalizedBody , 'base64' )
331+ ) ;
332+ } ) ;
333+ } ) ;
334+
335+ describe ( 'when Rekor returns an incomplete response' , ( ) => {
336+ describe ( 'when logId is missing' , ( ) => {
337+ const incompleteEntry = {
338+ ...rekorEntry ,
339+ logId : undefined ,
340+ } ;
341+
342+ beforeEach ( ( ) => {
343+ nock ( rekorBaseURL )
344+ . matchHeader ( 'Accept' , 'application/json' )
345+ . matchHeader ( 'Content-Type' , 'application/json' )
346+ . post ( '/api/v2/log/entries' )
347+ . reply ( 201 , incompleteEntry ) ;
348+ } ) ;
349+
350+ it ( 'returns an error' , async ( ) => {
351+ await expect (
352+ subject . createEntry ( createEntryRequest )
353+ ) . rejects . toThrowWithCode ( InternalError , 'TLOG_CREATE_ENTRY_ERROR' ) ;
354+ } ) ;
355+ } ) ;
356+
357+ describe ( 'when kindVersion is missing' , ( ) => {
358+ const incompleteEntry = {
359+ ...rekorEntry ,
360+ kindVersion : undefined ,
361+ } ;
362+
363+ beforeEach ( ( ) => {
364+ nock ( rekorBaseURL )
365+ . matchHeader ( 'Accept' , 'application/json' )
366+ . matchHeader ( 'Content-Type' , 'application/json' )
367+ . post ( '/api/v2/log/entries' )
368+ . reply ( 201 , incompleteEntry ) ;
369+ } ) ;
370+
371+ it ( 'returns an error' , async ( ) => {
372+ await expect (
373+ subject . createEntry ( createEntryRequest )
374+ ) . rejects . toThrowWithCode ( InternalError , 'TLOG_CREATE_ENTRY_ERROR' ) ;
375+ } ) ;
376+ } ) ;
377+ } ) ;
378+
379+ describe ( 'when Rekor returns a valid response after retry' , ( ) => {
380+ let scope : nock . Scope ;
381+
382+ beforeEach ( ( ) => {
383+ scope = nock ( rekorBaseURL )
384+ . post ( '/api/v2/log/entries' )
385+ . reply ( 500 , { message : 'oops' } )
386+ . post ( '/api/v2/log/entries' )
387+ . reply ( 201 , rekorEntry ) ;
388+ } ) ;
389+
390+ it ( 'returns a tlog entry' , async ( ) => {
391+ const subject = new TLogV2Client ( {
392+ rekorBaseURL,
393+ retry : { retries : 1 , factor : 0 , minTimeout : 1 , maxTimeout : 1 } ,
394+ } ) ;
395+ const entry = await subject . createEntry ( createEntryRequest ) ;
396+
397+ expect ( entry . logIndex ) . toEqual ( rekorEntry . logIndex ) ;
398+ } ) ;
399+ } ) ;
400+ } ) ;
401+ } ) ;
0 commit comments