diff --git a/acme/authorization.go b/acme/authorization.go index d2df5ea5..21cc9288 100644 --- a/acme/authorization.go +++ b/acme/authorization.go @@ -8,15 +8,16 @@ import ( // Authorization representst an ACME Authorization. type Authorization struct { - ID string `json:"-"` - AccountID string `json:"-"` - Token string `json:"-"` - Identifier Identifier `json:"identifier"` - Status Status `json:"status"` - Challenges []*Challenge `json:"challenges"` - Wildcard bool `json:"wildcard"` - ExpiresAt time.Time `json:"expires"` - Error *Error `json:"error,omitempty"` + ID string `json:"-"` + AccountID string `json:"-"` + Token string `json:"-"` + Identifier Identifier `json:"identifier"` + Status Status `json:"status"` + Challenges []*Challenge `json:"challenges"` + Wildcard bool `json:"wildcard"` + ExpiresAt time.Time `json:"expires"` + Fingerprint string `json:"fingerprint,omitempty"` + Error *Error `json:"error,omitempty"` } // ToLog enables response logging. diff --git a/acme/challenge.go b/acme/challenge.go index 7d1f4dee..4240d908 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -27,6 +27,7 @@ import ( "github.com/fxamacker/cbor/v2" "go.step.sm/crypto/jose" + "go.step.sm/crypto/keyutil" "go.step.sm/crypto/pemutil" "github.com/smallstep/certificates/authority/provisioner" @@ -347,6 +348,13 @@ type attestationObject struct { // TODO(bweeks): move attestation verification to a shared package. func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, payload []byte) error { + // Load authorization to store the key fingerprint. + az, err := db.GetAuthorization(ctx, ch.AuthorizationID) + if err != nil { + return WrapErrorISE(err, "error loading authorization") + } + + // Parse payload. var p payloadType if err := json.Unmarshal(payload, &p); err != nil { return WrapErrorISE(err, "error unmarshalling JSON") @@ -385,7 +393,6 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose } return WrapErrorISE(err, "error validating attestation") } - // Validate nonce with SHA-256 of the token. if len(data.Nonce) != 0 { sum := sha256.Sum256([]byte(ch.Token)) @@ -401,6 +408,9 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose if data.UDID != ch.Value && data.SerialNumber != ch.Value { return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match")) } + + // Update attestation key fingerprint to compare against the CSR + az.Fingerprint = data.Fingerprint case "step": data, err := doStepAttestationFormat(ctx, prov, ch, jwk, &att) if err != nil { @@ -426,6 +436,9 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose ) return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match").AddSubproblems(subproblem)) } + + // Update attestation key fingerprint to compare against the CSR + az.Fingerprint = data.Fingerprint default: return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "unexpected attestation object format")) } @@ -435,6 +448,15 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose ch.Error = nil ch.ValidatedAt = clock.Now().Format(time.RFC3339) + // Store the fingerprint in the authorization. + // + // TODO: add method to update authorization and challenge atomically. + if az.Fingerprint != "" { + if err := db.UpdateAuthorization(ctx, az); err != nil { + return WrapErrorISE(err, "error updating authorization") + } + } + if err := db.UpdateChallenge(ctx, ch); err != nil { return WrapErrorISE(err, "error updating challenge") } @@ -471,6 +493,7 @@ type appleAttestationData struct { UDID string SEPVersion string Certificate *x509.Certificate + Fingerprint string } func doAppleAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, att *attestationObject) (*appleAttestationData, error) { @@ -527,6 +550,9 @@ func doAppleAttestationFormat(ctx context.Context, prov Provisioner, ch *Challen data := &appleAttestationData{ Certificate: leaf, } + if data.Fingerprint, err = keyutil.Fingerprint(leaf.PublicKey); err != nil { + return nil, WrapErrorISE(err, "error calculating key fingerprint") + } for _, ext := range leaf.Extensions { switch { case ext.Id.Equal(oidAppleSerialNumber): @@ -572,6 +598,7 @@ var oidYubicoSerialNumber = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 41482, 3, 7} type stepAttestationData struct { Certificate *x509.Certificate SerialNumber string + Fingerprint string } func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *attestationObject) (*stepAttestationData, error) { @@ -667,6 +694,9 @@ func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challeng data := &stepAttestationData{ Certificate: leaf, } + if data.Fingerprint, err = keyutil.Fingerprint(leaf.PublicKey); err != nil { + return nil, WrapErrorISE(err, "error calculating key fingerprint") + } for _, ext := range leaf.Extensions { if !ext.Id.Equal(oidYubicoSerialNumber) { continue diff --git a/acme/challenge_test.go b/acme/challenge_test.go index fb94d8a7..ccd8f6b8 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -54,6 +54,13 @@ func (m *mockClient) TLSDial(network, addr string, tlsConfig *tls.Config) (*tls. return m.tlsDial(network, addr, tlsConfig) } +func fatalError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatal(err) + } +} + func mustNonAttestationProvisioner(t *testing.T) Provisioner { t.Helper() @@ -88,6 +95,108 @@ func mustAttestationProvisioner(t *testing.T, roots []byte) Provisioner { return prov } +func mustAccountAndKeyAuthorization(t *testing.T, token string) (*jose.JSONWebKey, string) { + t.Helper() + + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + fatalError(t, err) + + keyAuth, err := KeyAuthorization(token, jwk) + fatalError(t, err) + return jwk, keyAuth +} + +func mustAttestApple(t *testing.T, nonce string) ([]byte, *x509.Certificate, *x509.Certificate) { + t.Helper() + + ca, err := minica.New() + fatalError(t, err) + + signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + fatalError(t, err) + + nonceSum := sha256.Sum256([]byte(nonce)) + leaf, err := ca.Sign(&x509.Certificate{ + Subject: pkix.Name{CommonName: "attestation cert"}, + PublicKey: signer.Public(), + ExtraExtensions: []pkix.Extension{ + {Id: oidAppleSerialNumber, Value: []byte("serial-number")}, + {Id: oidAppleUniqueDeviceIdentifier, Value: []byte("udid")}, + {Id: oidAppleSecureEnclaveProcessorOSVersion, Value: []byte("16.0")}, + {Id: oidAppleNonce, Value: nonceSum[:]}, + }, + }) + fatalError(t, err) + + attObj, err := cbor.Marshal(struct { + Format string `json:"fmt"` + AttStatement map[string]interface{} `json:"attStmt,omitempty"` + }{ + Format: "apple", + AttStatement: map[string]interface{}{ + "x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw}, + }, + }) + fatalError(t, err) + + payload, err := json.Marshal(struct { + AttObj string `json:"attObj"` + }{ + AttObj: base64.RawURLEncoding.EncodeToString(attObj), + }) + fatalError(t, err) + + return payload, leaf, ca.Root +} + +func mustAttestYubikey(t *testing.T, nonce, keyAuthorization string, serial int) ([]byte, *x509.Certificate, *x509.Certificate) { + ca, err := minica.New() + fatalError(t, err) + + keyAuthSum := sha256.Sum256([]byte(keyAuthorization)) + + signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + fatalError(t, err) + sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256) + fatalError(t, err) + cborSig, err := cbor.Marshal(sig) + fatalError(t, err) + + serialNumber, err := asn1.Marshal(serial) + fatalError(t, err) + + leaf, err := ca.Sign(&x509.Certificate{ + Subject: pkix.Name{CommonName: "attestation cert"}, + PublicKey: signer.Public(), + ExtraExtensions: []pkix.Extension{ + {Id: oidYubicoSerialNumber, Value: serialNumber}, + }, + }) + fatalError(t, err) + + attObj, err := cbor.Marshal(struct { + Format string `json:"fmt"` + AttStatement map[string]interface{} `json:"attStmt,omitempty"` + }{ + Format: "step", + AttStatement: map[string]interface{}{ + "x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw}, + "alg": -7, + "sig": cborSig, + }, + }) + fatalError(t, err) + + payload, err := json.Marshal(struct { + AttObj string `json:"attObj"` + }{ + AttObj: base64.RawURLEncoding.EncodeToString(attObj), + }) + fatalError(t, err) + + return payload, leaf, ca.Root +} + func Test_storeError(t *testing.T) { type test struct { ch *Challenge @@ -661,13 +770,6 @@ func TestChallenge_Validate(t *testing.T) { } }, "fail/device-attest-01": func(t *testing.T) test { - ch := &Challenge{ - ID: "chID", - Token: "token", - Type: "device-attest-01", - Status: StatusPending, - Value: "12345678", - } payload, err := json.Marshal(struct { Error string `json:"error"` }{ @@ -675,9 +777,20 @@ func TestChallenge_Validate(t *testing.T) { }) assert.NoError(t, err) return test{ - ch: ch, + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + }, payload: payload, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -699,76 +812,39 @@ func TestChallenge_Validate(t *testing.T) { } }, "ok/device-attest-01": func(t *testing.T) test { - ctx := context.Background() - ca, err := minica.New() - assert.NoError(t, err) - caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw}) - ctx = NewProvisionerContext(ctx, mustAttestationProvisioner(t, caRoot)) - makeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate { - leaf, err := ca.Sign(&x509.Certificate{ - Subject: pkix.Name{CommonName: "attestation cert"}, - PublicKey: signer.Public(), - ExtraExtensions: []pkix.Extension{ - {Id: oidYubicoSerialNumber, Value: serialNumber}, - }, - }) - if err != nil { - t.Fatal(err) - } - return leaf - } - - signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - assert.NoError(t, err) - serialNumber, err := asn1.Marshal(1234) - assert.NoError(t, err) - leaf := makeLeaf(signer, serialNumber) + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + payload, leaf, root := mustAttestYubikey(t, "nonce", keyAuth, 1234) - jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) - assert.NoError(t, err) - token := "token" - keyAuth, err := KeyAuthorization(token, jwk) - assert.NoError(t, err) - keyAuthSum := sha256.Sum256([]byte(keyAuth)) - sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256) - assert.NoError(t, err) - cborSig, err := cbor.Marshal(sig) - assert.NoError(t, err) + caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw}) + ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot)) - ch := &Challenge{ - ID: "chID", - Token: token, - Type: "device-attest-01", - Status: StatusPending, - Value: "1234", - } - attObj, err := cbor.Marshal(struct { - Format string `json:"fmt"` - AttStatement map[string]interface{} `json:"attStmt,omitempty"` - }{ - Format: "step", - AttStatement: map[string]interface{}{ - "x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw}, - "alg": -7, - "sig": cborSig, - }, - }) - assert.NoError(t, err) - payload, err := json.Marshal(struct { - AttObj string `json:"attObj"` - }{ - AttObj: base64.RawURLEncoding.EncodeToString(attObj), - }) - assert.NoError(t, err) return test{ - ch: ch, + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "1234", + }, payload: payload, ctx: ctx, jwk: jwk, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, + MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error { + fingerprint, err := keyutil.Fingerprint(leaf.PublicKey) + assert.NoError(t, err) + assert.Equal(t, "azID", az.ID) + assert.Equal(t, fingerprint, az.Fingerprint) + return nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) - assert.Equal(t, token, updch.Token) + assert.Equal(t, "token", updch.Token) assert.Equal(t, StatusValid, updch.Status) assert.Equal(t, ChallengeType("device-attest-01"), updch.Type) assert.Equal(t, "1234", updch.Value) @@ -2745,6 +2821,10 @@ func Test_doAppleAttestationFormat(t *testing.T) { if err != nil { t.Fatal(err) } + fingerprint, err := keyutil.Fingerprint(signer.Public()) + if err != nil { + t.Fatal(err) + } type args struct { ctx context.Context @@ -2769,6 +2849,7 @@ func Test_doAppleAttestationFormat(t *testing.T) { UDID: "udid", SEPVersion: "16.0", Certificate: leaf, + Fingerprint: fingerprint, }, false}, {"fail apple issuer", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{}, &attestationObject{ Format: "apple", @@ -2871,6 +2952,10 @@ func Test_doStepAttestationFormat(t *testing.T) { t.Fatal(err) } leaf := makeLeaf(signer, serialNumber) + fingerprint, err := keyutil.Fingerprint(signer.Public()) + if err != nil { + t.Fatal(err) + } jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) if err != nil { @@ -2926,6 +3011,7 @@ func Test_doStepAttestationFormat(t *testing.T) { }}, &stepAttestationData{ SerialNumber: "1234", Certificate: leaf, + Fingerprint: fingerprint, }, false}, {"fail yubico issuer", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{Token: "token"}, jwk, &attestationObject{ Format: "step", @@ -3196,15 +3282,43 @@ func Test_deviceAttest01Validate(t *testing.T) { wantErr *Error } tests := map[string]func(t *testing.T) test{ + "fail/getAuthorization": func(t *testing.T) test { + return test{ + args: args{ + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + }, + db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + return nil, errors.New("not found") + }, + }, + payload: []byte(invalidPayload), + }, + wantErr: NewErrorISE("error loading authorization: not found"), + } + }, "fail/json.Unmarshal": func(t *testing.T) test { return test{ args: args{ ch: &Challenge{ - ID: "chID", - Token: "token", - Type: "device-attest-01", - Status: StatusPending, - Value: "12345678", + ID: "chID", + AuthorizationID: "azID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + }, + db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, }, payload: []byte(invalidPayload), }, @@ -3216,14 +3330,19 @@ func Test_deviceAttest01Validate(t *testing.T) { return test{ args: args{ ch: &Challenge{ - ID: "chID", - Token: "token", - Type: "device-attest-01", - Status: StatusPending, - Value: "12345678", + ID: "chID", + AuthorizationID: "azID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", }, payload: errorPayload, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -3250,14 +3369,19 @@ func Test_deviceAttest01Validate(t *testing.T) { return test{ args: args{ ch: &Challenge{ - ID: "chID", - Token: "token", - Type: "device-attest-01", - Status: StatusPending, - Value: "12345678", + ID: "chID", + AuthorizationID: "azID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", }, payload: errorPayload, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -3284,11 +3408,18 @@ func Test_deviceAttest01Validate(t *testing.T) { return test{ args: args{ ch: &Challenge{ - ID: "chID", - Token: "token", - Type: "device-attest-01", - Status: StatusPending, - Value: "12345678", + ID: "chID", + AuthorizationID: "azID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + }, + db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, }, payload: errorBase64Payload, }, @@ -3299,11 +3430,18 @@ func Test_deviceAttest01Validate(t *testing.T) { return test{ args: args{ ch: &Challenge{ - ID: "chID", - Token: "token", - Type: "device-attest-01", - Status: StatusPending, - Value: "12345678", + ID: "chID", + AuthorizationID: "azID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + }, + db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, }, payload: errorCBORPayload, }, @@ -3311,67 +3449,28 @@ func Test_deviceAttest01Validate(t *testing.T) { } }, "ok/prov.IsAttestationFormatEnabled": func(t *testing.T) test { - ca, err := minica.New() - require.NoError(t, err) - makeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate { - leaf, err := ca.Sign(&x509.Certificate{ - Subject: pkix.Name{CommonName: "attestation cert"}, - PublicKey: signer.Public(), - ExtraExtensions: []pkix.Extension{ - {Id: oidYubicoSerialNumber, Value: serialNumber}, - }, - }) - if err != nil { - t.Fatal(err) - } - return leaf - } - signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - serialNumber, err := asn1.Marshal(1234) - require.NoError(t, err) - leaf := makeLeaf(signer, serialNumber) - jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) - require.NoError(t, err) - token := "token" - keyAuth, err := KeyAuthorization(token, jwk) - require.NoError(t, err) - keyAuthSum := sha256.Sum256([]byte(keyAuth)) - sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256) - require.NoError(t, err) - cborSig, err := cbor.Marshal(sig) - require.NoError(t, err) + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + payload, _, _ := mustAttestYubikey(t, "nonce", keyAuth, 12345678) ctx := NewProvisionerContext(context.Background(), mustNonAttestationProvisioner(t)) - attObj, err := cbor.Marshal(struct { - Format string `json:"fmt"` - AttStatement map[string]interface{} `json:"attStmt,omitempty"` - }{ - Format: "step", - AttStatement: map[string]interface{}{ - "x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw}, - "alg": -7, - "sig": cborSig, - }, - }) - require.NoError(t, err) - payload, err := json.Marshal(struct { - AttObj string `json:"attObj"` - }{ - AttObj: base64.RawURLEncoding.EncodeToString(attObj), - }) - require.NoError(t, err) + return test{ args: args{ ctx: ctx, + jwk: jwk, ch: &Challenge{ - ID: "chID", - Token: "token", - Type: "device-attest-01", - Status: StatusPending, - Value: "12345678", + ID: "chID", + AuthorizationID: "azID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", }, payload: payload, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -3414,14 +3513,19 @@ func Test_deviceAttest01Validate(t *testing.T) { args: args{ ctx: ctx, ch: &Challenge{ - ID: "chID", - Token: "token", - Type: "device-attest-01", - Status: StatusPending, - Value: "12345678", + ID: "chID", + AuthorizationID: "azID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", }, payload: payload, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -3445,57 +3549,36 @@ func Test_deviceAttest01Validate(t *testing.T) { } }, "ok/doAppleAttestationFormat-non-matching-nonce": func(t *testing.T) test { - ca, err := minica.New() - require.NoError(t, err) - signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw}) - leaf, err := ca.Sign(&x509.Certificate{ - Subject: pkix.Name{CommonName: "attestation cert"}, - PublicKey: signer.Public(), - ExtraExtensions: []pkix.Extension{ - {Id: oidAppleSerialNumber, Value: []byte("serial-number")}, - {Id: oidAppleUniqueDeviceIdentifier, Value: []byte("udid")}, - {Id: oidAppleSecureEnclaveProcessorOSVersion, Value: []byte("16.0")}, - {Id: oidAppleNonce, Value: []byte("nonce")}, - }, - }) - require.NoError(t, err) + jwk, _ := mustAccountAndKeyAuthorization(t, "token") + payload, _, root := mustAttestApple(t, "bad-nonce") + + caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw}) ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot)) - attObj, err := cbor.Marshal(struct { - Format string `json:"fmt"` - AttStatement map[string]interface{} `json:"attStmt,omitempty"` - }{ - Format: "apple", - AttStatement: map[string]interface{}{ - "x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw}, - }, - }) - require.NoError(t, err) - payload, err := json.Marshal(struct { - AttObj string `json:"attObj"` - }{ - AttObj: base64.RawURLEncoding.EncodeToString(attObj), - }) - require.NoError(t, err) + return test{ args: args{ ctx: ctx, + jwk: jwk, ch: &Challenge{ - ID: "chID", - Token: "token", - Type: "device-attest-01", - Status: StatusPending, - Value: "12345678", + ID: "chID", + AuthorizationID: "azID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "serial-number", }, payload: payload, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) assert.Equal(t, StatusInvalid, updch.Status) assert.Equal(t, ChallengeType("device-attest-01"), updch.Type) - assert.Equal(t, "12345678", updch.Value) + assert.Equal(t, "serial-number", updch.Value) err := NewError(ErrorBadAttestationStatementType, "challenge token does not match") @@ -3513,52 +3596,29 @@ func Test_deviceAttest01Validate(t *testing.T) { } }, "ok/doAppleAttestationFormat-non-matching-challenge-value": func(t *testing.T) test { - ca, err := minica.New() - require.NoError(t, err) - signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw}) - nonce := sha256.Sum256([]byte("nonce")) - leaf, err := ca.Sign(&x509.Certificate{ - Subject: pkix.Name{CommonName: "attestation cert"}, - PublicKey: signer.Public(), - ExtraExtensions: []pkix.Extension{ - {Id: oidAppleSerialNumber, Value: []byte("serial-number")}, - {Id: oidAppleUniqueDeviceIdentifier, Value: []byte("udid")}, - {Id: oidAppleSecureEnclaveProcessorOSVersion, Value: []byte("16.0")}, - {Id: oidAppleNonce, Value: nonce[:]}, - }, - }) - require.NoError(t, err) + jwk, _ := mustAccountAndKeyAuthorization(t, "token") + payload, _, root := mustAttestApple(t, "nonce") + + caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw}) ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot)) - attObj, err := cbor.Marshal(struct { - Format string `json:"fmt"` - AttStatement map[string]interface{} `json:"attStmt,omitempty"` - }{ - Format: "apple", - AttStatement: map[string]interface{}{ - "x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw}, - }, - }) - require.NoError(t, err) - payload, err := json.Marshal(struct { - AttObj string `json:"attObj"` - }{ - AttObj: base64.RawURLEncoding.EncodeToString(attObj), - }) - require.NoError(t, err) return test{ args: args{ ctx: ctx, + jwk: jwk, ch: &Challenge{ - ID: "chID", - Token: "nonce", - Type: "device-attest-01", - Status: StatusPending, - Value: "non-matching-value", + ID: "chID", + AuthorizationID: "azID", + Token: "nonce", + Type: "device-attest-01", + Status: StatusPending, + Value: "non-matching-value", }, payload: payload, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "nonce", updch.Token) @@ -3619,14 +3679,19 @@ func Test_deviceAttest01Validate(t *testing.T) { args: args{ ctx: ctx, ch: &Challenge{ - ID: "chID", - Token: "token", - Type: "device-attest-01", - Status: StatusPending, - Value: "12345678", + ID: "chID", + AuthorizationID: "azID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", }, payload: payload, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -3650,69 +3715,37 @@ func Test_deviceAttest01Validate(t *testing.T) { } }, "ok/doStepAttestationFormat-non-matching-identifier": func(t *testing.T) test { - ca, err := minica.New() - require.NoError(t, err) - caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw}) - signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) - require.NoError(t, err) - token := "token" - keyAuth, err := KeyAuthorization(token, jwk) - require.NoError(t, err) - keyAuthSum := sha256.Sum256([]byte(keyAuth)) - sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256) - require.NoError(t, err) - cborSig, err := cbor.Marshal(sig) - require.NoError(t, err) + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + payload, leaf, root := mustAttestYubikey(t, "nonce", keyAuth, 87654321) + + caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw}) ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot)) - makeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate { - leaf, err := ca.Sign(&x509.Certificate{ - Subject: pkix.Name{CommonName: "attestation cert"}, - PublicKey: signer.Public(), - ExtraExtensions: []pkix.Extension{ - {Id: oidYubicoSerialNumber, Value: serialNumber}, - }, - }) - if err != nil { - t.Fatal(err) - } - return leaf - } - require.NoError(t, err) - serialNumber, err := asn1.Marshal(87654321) - require.NoError(t, err) - leaf := makeLeaf(signer, serialNumber) - attObj, err := cbor.Marshal(struct { - Format string `json:"fmt"` - AttStatement map[string]interface{} `json:"attStmt,omitempty"` - }{ - Format: "step", - AttStatement: map[string]interface{}{ - "x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw}, - "alg": -7, - "sig": cborSig, - }, - }) - require.NoError(t, err) - payload, err := json.Marshal(struct { - AttObj string `json:"attObj"` - }{ - AttObj: base64.RawURLEncoding.EncodeToString(attObj), - }) - require.NoError(t, err) + return test{ args: args{ ctx: ctx, + jwk: jwk, ch: &Challenge{ - ID: "chID", - Token: "token", - Type: "device-attest-01", - Status: StatusPending, - Value: "12345678", + ID: "chID", + AuthorizationID: "azID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", }, payload: payload, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, + MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error { + fingerprint, err := keyutil.Fingerprint(leaf.PublicKey) + assert.NoError(t, err) + assert.Equal(t, "azID", az.ID) + assert.Equal(t, fingerprint, az.Fingerprint) + return nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -3736,7 +3769,6 @@ func Test_deviceAttest01Validate(t *testing.T) { return nil }, }, - jwk: jwk, }, wantErr: nil, } @@ -3796,14 +3828,19 @@ func Test_deviceAttest01Validate(t *testing.T) { args: args{ ctx: ctx, ch: &Challenge{ - ID: "chID", - Token: "token", - Type: "device-attest-01", - Status: StatusPending, - Value: "12345678", + ID: "chID", + AuthorizationID: "azID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", }, payload: payload, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -3827,70 +3864,75 @@ func Test_deviceAttest01Validate(t *testing.T) { wantErr: nil, } }, - "fail/db.UpdateChallenge": func(t *testing.T) test { - ca, err := minica.New() - require.NoError(t, err) - caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw}) - signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) - require.NoError(t, err) - token := "token" - keyAuth, err := KeyAuthorization(token, jwk) - require.NoError(t, err) - keyAuthSum := sha256.Sum256([]byte(keyAuth)) - sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256) - require.NoError(t, err) - cborSig, err := cbor.Marshal(sig) - require.NoError(t, err) + "fail/db.UpdateAuthorization": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + payload, leaf, root := mustAttestYubikey(t, "nonce", keyAuth, 12345678) + + caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw}) ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot)) - makeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate { - leaf, err := ca.Sign(&x509.Certificate{ - Subject: pkix.Name{CommonName: "attestation cert"}, - PublicKey: signer.Public(), - ExtraExtensions: []pkix.Extension{ - {Id: oidYubicoSerialNumber, Value: serialNumber}, + + return test{ + args: args{ + ctx: ctx, + jwk: jwk, + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + }, + payload: payload, + db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, + MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error { + fingerprint, err := keyutil.Fingerprint(leaf.PublicKey) + assert.NoError(t, err) + assert.Equal(t, "azID", az.ID) + assert.Equal(t, fingerprint, az.Fingerprint) + return errors.New("force") + }, }, - }) - if err != nil { - t.Fatal(err) - } - return leaf - } - require.NoError(t, err) - serialNumber, err := asn1.Marshal(12345678) - require.NoError(t, err) - leaf := makeLeaf(signer, serialNumber) - attObj, err := cbor.Marshal(struct { - Format string `json:"fmt"` - AttStatement map[string]interface{} `json:"attStmt,omitempty"` - }{ - Format: "step", - AttStatement: map[string]interface{}{ - "x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw}, - "alg": -7, - "sig": cborSig, }, - }) - require.NoError(t, err) - payload, err := json.Marshal(struct { - AttObj string `json:"attObj"` - }{ - AttObj: base64.RawURLEncoding.EncodeToString(attObj), - }) - require.NoError(t, err) + wantErr: NewError(ErrorServerInternalType, "error updating authorization: force"), + } + }, + "fail/db.UpdateChallenge": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + payload, leaf, root := mustAttestYubikey(t, "nonce", keyAuth, 12345678) + + caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw}) + ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot)) + return test{ args: args{ ctx: ctx, + jwk: jwk, ch: &Challenge{ - ID: "chID", - Token: "token", - Type: "device-attest-01", - Status: StatusPending, - Value: "12345678", + ID: "chID", + AuthorizationID: "azID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", }, payload: payload, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, + MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error { + fingerprint, err := keyutil.Fingerprint(leaf.PublicKey) + assert.NoError(t, err) + assert.Equal(t, "azID", az.ID) + assert.Equal(t, fingerprint, az.Fingerprint) + return nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -3901,75 +3943,42 @@ func Test_deviceAttest01Validate(t *testing.T) { return errors.New("force") }, }, - jwk: jwk, }, wantErr: NewError(ErrorServerInternalType, "error updating challenge: force"), } }, "ok": func(t *testing.T) test { - ca, err := minica.New() - require.NoError(t, err) - caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw}) - signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) - require.NoError(t, err) - token := "token" - keyAuth, err := KeyAuthorization(token, jwk) - require.NoError(t, err) - keyAuthSum := sha256.Sum256([]byte(keyAuth)) - sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256) - require.NoError(t, err) - cborSig, err := cbor.Marshal(sig) - require.NoError(t, err) + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + payload, leaf, root := mustAttestYubikey(t, "nonce", keyAuth, 12345678) + + caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw}) ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot)) - makeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate { - leaf, err := ca.Sign(&x509.Certificate{ - Subject: pkix.Name{CommonName: "attestation cert"}, - PublicKey: signer.Public(), - ExtraExtensions: []pkix.Extension{ - {Id: oidYubicoSerialNumber, Value: serialNumber}, - }, - }) - if err != nil { - t.Fatal(err) - } - return leaf - } - require.NoError(t, err) - serialNumber, err := asn1.Marshal(12345678) - require.NoError(t, err) - leaf := makeLeaf(signer, serialNumber) - attObj, err := cbor.Marshal(struct { - Format string `json:"fmt"` - AttStatement map[string]interface{} `json:"attStmt,omitempty"` - }{ - Format: "step", - AttStatement: map[string]interface{}{ - "x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw}, - "alg": -7, - "sig": cborSig, - }, - }) - require.NoError(t, err) - payload, err := json.Marshal(struct { - AttObj string `json:"attObj"` - }{ - AttObj: base64.RawURLEncoding.EncodeToString(attObj), - }) - require.NoError(t, err) + return test{ args: args{ ctx: ctx, + jwk: jwk, ch: &Challenge{ - ID: "chID", - Token: "token", - Type: "device-attest-01", - Status: StatusPending, - Value: "12345678", + ID: "chID", + AuthorizationID: "azID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", }, payload: payload, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + assert.Equal(t, "azID", id) + return &Authorization{ID: "azID"}, nil + }, + MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error { + fingerprint, err := keyutil.Fingerprint(leaf.PublicKey) + assert.NoError(t, err) + assert.Equal(t, "azID", az.ID) + assert.Equal(t, fingerprint, az.Fingerprint) + return nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -3980,7 +3989,6 @@ func Test_deviceAttest01Validate(t *testing.T) { return nil }, }, - jwk: jwk, }, wantErr: nil, } diff --git a/acme/db/nosql/authz.go b/acme/db/nosql/authz.go index 01cb7fed..d63aa89e 100644 --- a/acme/db/nosql/authz.go +++ b/acme/db/nosql/authz.go @@ -17,6 +17,7 @@ type dbAuthz struct { Identifier acme.Identifier `json:"identifier"` Status acme.Status `json:"status"` Token string `json:"token"` + Fingerprint string `json:"fingerprint,omitempty"` ChallengeIDs []string `json:"challengeIDs"` Wildcard bool `json:"wildcard"` CreatedAt time.Time `json:"createdAt"` @@ -61,15 +62,16 @@ func (db *DB) GetAuthorization(ctx context.Context, id string) (*acme.Authorizat } } return &acme.Authorization{ - ID: dbaz.ID, - AccountID: dbaz.AccountID, - Identifier: dbaz.Identifier, - Status: dbaz.Status, - Challenges: chs, - Wildcard: dbaz.Wildcard, - ExpiresAt: dbaz.ExpiresAt, - Token: dbaz.Token, - Error: dbaz.Error, + ID: dbaz.ID, + AccountID: dbaz.AccountID, + Identifier: dbaz.Identifier, + Status: dbaz.Status, + Challenges: chs, + Wildcard: dbaz.Wildcard, + ExpiresAt: dbaz.ExpiresAt, + Token: dbaz.Token, + Fingerprint: dbaz.Fingerprint, + Error: dbaz.Error, }, nil } @@ -97,6 +99,7 @@ func (db *DB) CreateAuthorization(ctx context.Context, az *acme.Authorization) e Identifier: az.Identifier, ChallengeIDs: chIDs, Token: az.Token, + Fingerprint: az.Fingerprint, Wildcard: az.Wildcard, } @@ -111,8 +114,8 @@ func (db *DB) UpdateAuthorization(ctx context.Context, az *acme.Authorization) e } nu := old.clone() - nu.Status = az.Status + nu.Fingerprint = az.Fingerprint nu.Error = az.Error return db.save(ctx, old.ID, nu, old, "authz", authzTable) } @@ -136,15 +139,16 @@ func (db *DB) GetAuthorizationsByAccountID(ctx context.Context, accountID string continue } authzs = append(authzs, &acme.Authorization{ - ID: dbaz.ID, - AccountID: dbaz.AccountID, - Identifier: dbaz.Identifier, - Status: dbaz.Status, - Challenges: nil, // challenges not required for current use case - Wildcard: dbaz.Wildcard, - ExpiresAt: dbaz.ExpiresAt, - Token: dbaz.Token, - Error: dbaz.Error, + ID: dbaz.ID, + AccountID: dbaz.AccountID, + Identifier: dbaz.Identifier, + Status: dbaz.Status, + Challenges: nil, // challenges not required for current use case + Wildcard: dbaz.Wildcard, + ExpiresAt: dbaz.ExpiresAt, + Token: dbaz.Token, + Fingerprint: dbaz.Fingerprint, + Error: dbaz.Error, }) } diff --git a/acme/db/nosql/authz_test.go b/acme/db/nosql/authz_test.go index c7d47eda..6bc2b94d 100644 --- a/acme/db/nosql/authz_test.go +++ b/acme/db/nosql/authz_test.go @@ -473,6 +473,7 @@ func TestDB_UpdateAuthorization(t *testing.T) { ExpiresAt: now.Add(5 * time.Minute), ChallengeIDs: []string{"foo", "bar"}, Wildcard: true, + Fingerprint: "fingerprint", } b, err := json.Marshal(dbaz) assert.FatalError(t, err) @@ -549,10 +550,11 @@ func TestDB_UpdateAuthorization(t *testing.T) { {ID: "foo"}, {ID: "bar"}, }, - Token: dbaz.Token, - Wildcard: dbaz.Wildcard, - ExpiresAt: dbaz.ExpiresAt, - Error: acme.NewError(acme.ErrorMalformedType, "malformed"), + Token: dbaz.Token, + Wildcard: dbaz.Wildcard, + ExpiresAt: dbaz.ExpiresAt, + Fingerprint: "fingerprint", + Error: acme.NewError(acme.ErrorMalformedType, "malformed"), } return test{ az: updAz, @@ -582,6 +584,7 @@ func TestDB_UpdateAuthorization(t *testing.T) { assert.Equals(t, dbNew.Wildcard, dbaz.Wildcard) assert.Equals(t, dbNew.CreatedAt, dbaz.CreatedAt) assert.Equals(t, dbNew.ExpiresAt, dbaz.ExpiresAt) + assert.Equals(t, dbNew.Fingerprint, dbaz.Fingerprint) assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error()) return nu, true, nil }, diff --git a/acme/order.go b/acme/order.go index f5aac95a..8dfcf97a 100644 --- a/acme/order.go +++ b/acme/order.go @@ -3,6 +3,7 @@ package acme import ( "bytes" "context" + "crypto/subtle" "crypto/x509" "encoding/json" "net" @@ -11,6 +12,7 @@ import ( "time" "github.com/smallstep/certificates/authority/provisioner" + "go.step.sm/crypto/keyutil" "go.step.sm/crypto/x509util" ) @@ -125,6 +127,27 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error { return nil } +// getKeyFingerprint returns a fingerprint from the list of authorizations. This +// fingerprint is used on the device-attest-01 flow to verify the attestation +// certificate public key with the CSR public key. +// +// There's no point on reading all the authorizations as there will be only one +// for a permanent identifier. +func (o *Order) getAuthorizationFingerprint(ctx context.Context, db DB) (string, error) { + for _, azID := range o.AuthorizationIDs { + az, err := db.GetAuthorization(ctx, azID) + if err != nil { + return "", WrapErrorISE(err, "error getting authorization %q", azID) + } + // There's no point on reading all the authorizations as there will + // be only one for a permanent identifier. + if az.Fingerprint != "" { + return az.Fingerprint, nil + } + } + return "", nil +} + // Finalize signs a certificate if the necessary conditions for Order completion // have been met. // @@ -150,6 +173,24 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques return NewErrorISE("unexpected status %s for order %s", o.Status, o.ID) } + // Get key fingerprint if any. And then compare it with the CSR fingerprint. + // + // In device-attest-01 challenges we should check that the keys in the CSR + // and the attestation certificate are the same. + fingerprint, err := o.getAuthorizationFingerprint(ctx, db) + if err != nil { + return err + } + if fingerprint != "" { + fp, err := keyutil.Fingerprint(csr.PublicKey) + if err != nil { + return WrapErrorISE(err, "error calculating key fingerprint") + } + if subtle.ConstantTimeCompare([]byte(fingerprint), []byte(fp)) == 0 { + return NewError(ErrorUnauthorizedType, "order %s csr does not match the attested key", o.ID) + } + } + // canonicalize the CSR to allow for comparison csr = canonicalize(csr) diff --git a/acme/order_test.go b/acme/order_test.go index 133eec25..b8018c7b 100644 --- a/acme/order_test.go +++ b/acme/order_test.go @@ -2,6 +2,7 @@ package acme import ( "context" + "crypto" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" @@ -18,6 +19,7 @@ import ( "github.com/smallstep/assert" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" + "go.step.sm/crypto/keyutil" "go.step.sm/crypto/x509util" ) @@ -308,6 +310,14 @@ func (m *mockSignAuth) Revoke(context.Context, *authority.RevokeOptions) error { } func TestOrder_Finalize(t *testing.T) { + mustSigner := func(kty, crv string, size int) crypto.Signer { + s, err := keyutil.GenerateSigner(kty, crv, size) + if err != nil { + t.Fatal(err) + } + return s + } + type test struct { o *Order err *Error @@ -400,10 +410,18 @@ func TestOrder_Finalize(t *testing.T) { {Type: "permanent-identifier", Value: "a-permanent-identifier"}, }, } + + signer := mustSigner("EC", "P-256", 0) + fingerprint, err := keyutil.Fingerprint(signer.Public()) + if err != nil { + t.Fatal(err) + } + csr := &x509.CertificateRequest{ Subject: pkix.Name{ CommonName: "a-different-identifier", }, + PublicKey: signer.Public(), ExtraExtensions: []pkix.Extension{ { Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3}, @@ -414,6 +432,29 @@ func TestOrder_Finalize(t *testing.T) { return test{ o: o, csr: csr, + db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + switch id { + case "a": + return &Authorization{ + ID: id, + Status: StatusValid, + }, nil + case "b": + return &Authorization{ + ID: id, + Fingerprint: fingerprint, + Status: StatusValid, + }, nil + default: + assert.FatalError(t, errors.Errorf("unexpected authorization %s", id)) + return nil, errors.New("force") + } + }, + MockUpdateOrder: func(ctx context.Context, o *Order) error { + return nil + }, + }, err: &Error{ Type: "urn:ietf:params:acme:error:badCSR", Detail: "The CSR is unacceptable", @@ -452,6 +493,11 @@ func TestOrder_Finalize(t *testing.T) { return nil, errors.New("force") }, }, + db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + return &Authorization{ID: id, Status: StatusValid}, nil + }, + }, err: NewErrorISE("error retrieving authorization options from ACME provisioner: force"), } }, @@ -491,6 +537,11 @@ func TestOrder_Finalize(t *testing.T) { } }, }, + db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + return &Authorization{ID: id, Status: StatusValid}, nil + }, + }, err: NewErrorISE("error creating template options from ACME provisioner: error unmarshaling template data: invalid character 'o' in literal false (expecting 'a')"), } }, @@ -532,6 +583,11 @@ func TestOrder_Finalize(t *testing.T) { return nil, errors.New("force") }, }, + db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + return &Authorization{ID: id, Status: StatusValid}, nil + }, + }, err: NewErrorISE("error signing certificate for order oID: force"), } }, @@ -578,6 +634,9 @@ func TestOrder_Finalize(t *testing.T) { }, }, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + return &Authorization{ID: id, Status: StatusValid}, nil + }, MockCreateCertificate: func(ctx context.Context, cert *Certificate) error { assert.Equals(t, cert.AccountID, o.AccountID) assert.Equals(t, cert.OrderID, o.ID) @@ -632,6 +691,9 @@ func TestOrder_Finalize(t *testing.T) { }, }, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + return &Authorization{ID: id, Status: StatusValid}, nil + }, MockCreateCertificate: func(ctx context.Context, cert *Certificate) error { cert.ID = "certID" assert.Equals(t, cert.AccountID, o.AccountID) @@ -654,6 +716,95 @@ func TestOrder_Finalize(t *testing.T) { err: NewErrorISE("error updating order oID: force"), } }, + "fail/csr-fingerprint": func(t *testing.T) test { + now := clock.Now() + o := &Order{ + ID: "oID", + AccountID: "accID", + Status: StatusReady, + ExpiresAt: now.Add(5 * time.Minute), + AuthorizationIDs: []string{"a", "b"}, + Identifiers: []Identifier{ + {Type: "permanent-identifier", Value: "a-permanent-identifier"}, + }, + } + + signer := mustSigner("EC", "P-256", 0) + + csr := &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: "a-permanent-identifier", + }, + PublicKey: signer.Public(), + ExtraExtensions: []pkix.Extension{ + { + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3}, + Value: []byte("a-permanent-identifier"), + }, + }, + } + + leaf := &x509.Certificate{ + Subject: pkix.Name{CommonName: "a-permanent-identifier"}, + PublicKey: signer.Public(), + ExtraExtensions: []pkix.Extension{ + { + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3}, + Value: []byte("a-permanent-identifier"), + }, + }, + } + inter := &x509.Certificate{Subject: pkix.Name{CommonName: "inter"}} + root := &x509.Certificate{Subject: pkix.Name{CommonName: "root"}} + + return test{ + o: o, + csr: csr, + prov: &MockProvisioner{ + MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) { + assert.Equals(t, token, "") + return nil, nil + }, + MgetOptions: func() *provisioner.Options { + return nil + }, + }, + ca: &mockSignAuth{ + sign: func(_csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) { + assert.Equals(t, _csr, csr) + return []*x509.Certificate{leaf, inter, root}, nil + }, + }, + db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + return &Authorization{ + ID: id, + Fingerprint: "other-fingerprint", + Status: StatusValid, + }, nil + }, + MockCreateCertificate: func(ctx context.Context, cert *Certificate) error { + cert.ID = "certID" + assert.Equals(t, cert.AccountID, o.AccountID) + assert.Equals(t, cert.OrderID, o.ID) + assert.Equals(t, cert.Leaf, leaf) + assert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root}) + return nil + }, + MockUpdateOrder: func(ctx context.Context, updo *Order) error { + assert.Equals(t, updo.CertificateID, "certID") + assert.Equals(t, updo.Status, StatusValid) + assert.Equals(t, updo.ID, o.ID) + assert.Equals(t, updo.AccountID, o.AccountID) + assert.Equals(t, updo.ExpiresAt, o.ExpiresAt) + assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs) + assert.Equals(t, updo.Identifiers, o.Identifiers) + return nil + }, + }, + err: NewError(ErrorUnauthorizedType, "order oID csr does not match the attested key"), + } + }, "ok/permanent-identifier": func(t *testing.T) test { now := clock.Now() o := &Order{ @@ -666,10 +817,18 @@ func TestOrder_Finalize(t *testing.T) { {Type: "permanent-identifier", Value: "a-permanent-identifier"}, }, } + + signer := mustSigner("EC", "P-256", 0) + fingerprint, err := keyutil.Fingerprint(signer.Public()) + if err != nil { + t.Fatal(err) + } + csr := &x509.CertificateRequest{ Subject: pkix.Name{ CommonName: "a-permanent-identifier", }, + PublicKey: signer.Public(), ExtraExtensions: []pkix.Extension{ { Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3}, @@ -679,7 +838,8 @@ func TestOrder_Finalize(t *testing.T) { } leaf := &x509.Certificate{ - Subject: pkix.Name{CommonName: "a-permanent-identifier"}, + Subject: pkix.Name{CommonName: "a-permanent-identifier"}, + PublicKey: signer.Public(), ExtraExtensions: []pkix.Extension{ { Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3}, @@ -709,6 +869,24 @@ func TestOrder_Finalize(t *testing.T) { }, }, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + switch id { + case "a": + return &Authorization{ + ID: id, + Status: StatusValid, + }, nil + case "b": + return &Authorization{ + ID: id, + Fingerprint: fingerprint, + Status: StatusValid, + }, nil + default: + assert.FatalError(t, errors.Errorf("unexpected authorization %s", id)) + return nil, errors.New("force") + } + }, MockCreateCertificate: func(ctx context.Context, cert *Certificate) error { cert.ID = "certID" assert.Equals(t, cert.AccountID, o.AccountID) @@ -743,11 +921,19 @@ func TestOrder_Finalize(t *testing.T) { {Type: "permanent-identifier", Value: "a-permanent-identifier"}, }, } + + signer := mustSigner("EC", "P-256", 0) + fingerprint, err := keyutil.Fingerprint(signer.Public()) + if err != nil { + t.Fatal(err) + } + csr := &x509.CertificateRequest{ Subject: pkix.Name{ CommonName: "a-permanent-identifier", }, - DNSNames: []string{"foo.internal"}, + DNSNames: []string{"foo.internal"}, + PublicKey: signer.Public(), ExtraExtensions: []pkix.Extension{ { Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3}, @@ -757,7 +943,8 @@ func TestOrder_Finalize(t *testing.T) { } leaf := &x509.Certificate{ - Subject: pkix.Name{CommonName: "a-permanent-identifier"}, + Subject: pkix.Name{CommonName: "a-permanent-identifier"}, + PublicKey: signer.Public(), ExtraExtensions: []pkix.Extension{ { Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3}, @@ -792,6 +979,13 @@ func TestOrder_Finalize(t *testing.T) { }, }, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + return &Authorization{ + ID: id, + Fingerprint: fingerprint, + Status: StatusValid, + }, nil + }, MockCreateCertificate: func(ctx context.Context, cert *Certificate) error { cert.ID = "certID" assert.Equals(t, cert.AccountID, o.AccountID) @@ -856,6 +1050,9 @@ func TestOrder_Finalize(t *testing.T) { }, }, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + return &Authorization{ID: id, Status: StatusValid}, nil + }, MockCreateCertificate: func(ctx context.Context, cert *Certificate) error { cert.ID = "certID" assert.Equals(t, cert.AccountID, o.AccountID) @@ -917,6 +1114,9 @@ func TestOrder_Finalize(t *testing.T) { }, }, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + return &Authorization{ID: id, Status: StatusValid}, nil + }, MockCreateCertificate: func(ctx context.Context, cert *Certificate) error { cert.ID = "certID" assert.Equals(t, cert.AccountID, o.AccountID) @@ -981,6 +1181,9 @@ func TestOrder_Finalize(t *testing.T) { }, }, db: &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + return &Authorization{ID: id, Status: StatusValid}, nil + }, MockCreateCertificate: func(ctx context.Context, cert *Certificate) error { cert.ID = "certID" assert.Equals(t, cert.AccountID, o.AccountID) @@ -1688,3 +1891,55 @@ func TestOrder_sans(t *testing.T) { }) } } + +func TestOrder_getAuthorizationFingerprint(t *testing.T) { + ctx := context.Background() + type fields struct { + AuthorizationIDs []string + } + type args struct { + ctx context.Context + db DB + } + tests := []struct { + name string + fields fields + args args + want string + wantErr bool + }{ + {"ok", fields{[]string{"az1", "az2"}}, args{ctx, &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + return &Authorization{ID: id, Status: StatusValid}, nil + }, + }}, "", false}, + {"ok fingerprint", fields{[]string{"az1", "az2"}}, args{ctx, &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + if id == "az1" { + return &Authorization{ID: id, Status: StatusValid}, nil + } + return &Authorization{ID: id, Fingerprint: "fingerprint", Status: StatusValid}, nil + }, + }}, "fingerprint", false}, + {"fail", fields{[]string{"az1", "az2"}}, args{ctx, &MockDB{ + MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { + return nil, errors.New("force") + }, + }}, "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &Order{ + AuthorizationIDs: tt.fields.AuthorizationIDs, + } + got, err := o.getAuthorizationFingerprint(tt.args.ctx, tt.args.db) + if (err != nil) != tt.wantErr { + t.Errorf("Order.getAuthorizationFingerprint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Order.getAuthorizationFingerprint() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/go.mod b/go.mod index fcfb57d3..1906b8f1 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Masterminds/sprig/v3 v3.2.3 github.com/ThalesIgnite/crypto11 v1.2.5 // indirect - github.com/aws/aws-sdk-go v1.44.185 // indirect + github.com/aws/aws-sdk-go v1.44.195 // indirect github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/fatih/color v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.4.0 @@ -43,7 +43,7 @@ require ( github.com/urfave/cli v1.22.12 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.step.sm/cli-utils v0.7.5 - go.step.sm/crypto v0.23.2 + go.step.sm/crypto v0.25.0 go.step.sm/linkedca v0.19.0 golang.org/x/crypto v0.5.0 golang.org/x/net v0.5.0 diff --git a/go.sum b/go.sum index 688c3ac2..020471f3 100644 --- a/go.sum +++ b/go.sum @@ -84,8 +84,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.44.185 h1:stasiou+Ucx2A0RyXRyPph4sLCBxVQK7DPPK8tNcl5g= -github.com/aws/aws-sdk-go v1.44.185/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.195 h1:d5xFL0N83Fpsq2LFiHgtBUHknCRUPGHdOlCWt/jtOJs= +github.com/aws/aws-sdk-go v1.44.195/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -690,8 +690,8 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe go.step.sm/cli-utils v0.7.5 h1:jyp6X8k8mN1B0uWJydTid0C++8tQhm2kaaAdXKQQzdk= go.step.sm/cli-utils v0.7.5/go.mod h1:taSsY8haLmXoXM3ZkywIyRmVij/4Aj0fQbNTlJvv71I= go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= -go.step.sm/crypto v0.23.2 h1:XGmQH9Pkpxop47cjYlUhF10L5roPCbu1BCZXopbeW8I= -go.step.sm/crypto v0.23.2/go.mod h1:/IXGz8al8k7u7OV0RTWIi8TRVqO2FMyZVpedV+6Da6U= +go.step.sm/crypto v0.25.0 h1:a+7sKyozZH9B30s0dHluygxreUxI1NtCBEmuNXx7a4k= +go.step.sm/crypto v0.25.0/go.mod h1:kr1rzO6SzeQnLm6Zu6lNtksHZLiFe9k8LolSJNhoc94= go.step.sm/linkedca v0.19.0 h1:xuagkR35wrJI2gnu6FAM+q3VmjwsHScvGcJsfZ0GdsI= go.step.sm/linkedca v0.19.0/go.mod h1:b7vWPrHfYLEOTSUZitFEcztVCpTc+ileIN85CwEAluM= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=